diff --git a/BusinessCard/Assets.xcassets/AppBackgroundAccent.colorset/Contents.json b/BusinessCard/Assets.xcassets/AppBackgroundAccent.colorset/Contents.json
index 46886a4..a460be2 100644
--- a/BusinessCard/Assets.xcassets/AppBackgroundAccent.colorset/Contents.json
+++ b/BusinessCard/Assets.xcassets/AppBackgroundAccent.colorset/Contents.json
@@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
- "blue" : "0.860",
- "green" : "0.910",
- "red" : "0.950"
+ "blue" : "0.975",
+ "green" : "0.940",
+ "red" : "0.915"
}
},
"idiom" : "universal"
diff --git a/BusinessCard/Assets.xcassets/AppBackgroundBase.colorset/Contents.json b/BusinessCard/Assets.xcassets/AppBackgroundBase.colorset/Contents.json
index 820d3cd..36123a8 100644
--- a/BusinessCard/Assets.xcassets/AppBackgroundBase.colorset/Contents.json
+++ b/BusinessCard/Assets.xcassets/AppBackgroundBase.colorset/Contents.json
@@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
- "blue" : "0.940",
- "green" : "0.960",
- "red" : "0.970"
+ "blue" : "0.965",
+ "green" : "0.940",
+ "red" : "0.920"
}
},
"idiom" : "universal"
diff --git a/BusinessCard/Assets.xcassets/AppBackgroundSecondary.colorset/Contents.json b/BusinessCard/Assets.xcassets/AppBackgroundSecondary.colorset/Contents.json
index ade7426..191284c 100644
--- a/BusinessCard/Assets.xcassets/AppBackgroundSecondary.colorset/Contents.json
+++ b/BusinessCard/Assets.xcassets/AppBackgroundSecondary.colorset/Contents.json
@@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
- "blue" : "0.950",
- "green" : "0.950",
- "red" : "0.950"
+ "blue" : "0.988",
+ "green" : "0.972",
+ "red" : "0.965"
}
},
"idiom" : "universal"
diff --git a/BusinessCard/BusinessCardApp.swift b/BusinessCard/BusinessCardApp.swift
index ed76070..e63d5ed 100644
--- a/BusinessCard/BusinessCardApp.swift
+++ b/BusinessCard/BusinessCardApp.swift
@@ -110,7 +110,7 @@ struct BusinessCardApp: App {
.environment(appState)
.preferredColorScheme(appState.preferredColorScheme)
.autocorrectionDisabled(true)
- .textInputAutocapitalization(.never)
+ .textInputAutocapitalization(.sentences)
}
}
}
diff --git a/BusinessCard/Configuration/Base.xcconfig b/BusinessCard/Configuration/Base.xcconfig
index 9811d9d..e56e3ea 100644
--- a/BusinessCard/Configuration/Base.xcconfig
+++ b/BusinessCard/Configuration/Base.xcconfig
@@ -26,7 +26,7 @@ UITESTS_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME)UITests
// Entitlement identifiers
APP_GROUP_IDENTIFIER = group.$(COMPANY_IDENTIFIER).$(APP_NAME)
CLOUDKIT_CONTAINER_IDENTIFIER = iCloud.$(COMPANY_IDENTIFIER).$(APP_NAME)
-CLOUDKIT_SYNC_ENABLED = NO
+CLOUDKIT_SYNC_ENABLED = YES
// =============================================================================
// APP CLIP CONFIGURATION
diff --git a/BusinessCard/Design/DesignConstants.swift b/BusinessCard/Design/DesignConstants.swift
index c0187ef..82cd3e9 100644
--- a/BusinessCard/Design/DesignConstants.swift
+++ b/BusinessCard/Design/DesignConstants.swift
@@ -203,7 +203,7 @@ private struct KeyboardDismissModifier: ViewModifier {
func body(content: Content) -> some View {
content
.autocorrectionDisabled(true)
- .textInputAutocapitalization(.never)
+ .textInputAutocapitalization(.sentences)
.scrollDismissesKeyboard(.interactively)
}
}
diff --git a/BusinessCard/Models/SharedCardRecord.swift b/BusinessCard/Models/SharedCardRecord.swift
index ae90b22..8cff8ae 100644
--- a/BusinessCard/Models/SharedCardRecord.swift
+++ b/BusinessCard/Models/SharedCardRecord.swift
@@ -4,6 +4,9 @@ import CloudKit
/// NOT a SwiftData model - uses raw CKRecord for ephemeral sharing.
struct SharedCardRecord: Sendable {
let recordID: CKRecord.ID
+ let displayName: String
+ let role: String
+ let company: String
let vCardData: String
let expiresAt: Date
let createdAt: Date
@@ -11,13 +14,28 @@ struct SharedCardRecord: Sendable {
static let recordType = "SharedCard"
enum Field: String {
+ case displayName
+ case role
+ case company
case vCardData
+ case photoAsset
case expiresAt
case createdAt
}
- init(recordID: CKRecord.ID, vCardData: String, expiresAt: Date, createdAt: Date = .now) {
+ init(
+ recordID: CKRecord.ID,
+ displayName: String,
+ role: String,
+ company: String,
+ vCardData: String,
+ expiresAt: Date,
+ createdAt: Date = .now
+ ) {
self.recordID = recordID
+ self.displayName = displayName
+ self.role = role
+ self.company = company
self.vCardData = vCardData
self.expiresAt = expiresAt
self.createdAt = createdAt
@@ -29,14 +47,23 @@ struct SharedCardRecord: Sendable {
return nil
}
self.recordID = record.recordID
+ self.displayName = (record[Field.displayName.rawValue] as? String) ?? ""
+ self.role = (record[Field.role.rawValue] as? String) ?? ""
+ self.company = (record[Field.company.rawValue] as? String) ?? ""
self.vCardData = vCardData
self.expiresAt = expiresAt
self.createdAt = (record[Field.createdAt.rawValue] as? Date) ?? record.creationDate ?? .now
}
- func toCKRecord() -> CKRecord {
+ func toCKRecord(photoAssetFileURL: URL? = nil) -> CKRecord {
let record = CKRecord(recordType: Self.recordType, recordID: recordID)
+ record[Field.displayName.rawValue] = displayName
+ record[Field.role.rawValue] = role
+ record[Field.company.rawValue] = company
record[Field.vCardData.rawValue] = vCardData
+ if let photoAssetFileURL {
+ record[Field.photoAsset.rawValue] = CKAsset(fileURL: photoAssetFileURL)
+ }
record[Field.expiresAt.rawValue] = expiresAt
record[Field.createdAt.rawValue] = createdAt
return record
diff --git a/BusinessCard/Services/SharedCardCloudKitService.swift b/BusinessCard/Services/SharedCardCloudKitService.swift
index 17b6c47..3d613e2 100644
--- a/BusinessCard/Services/SharedCardCloudKitService.swift
+++ b/BusinessCard/Services/SharedCardCloudKitService.swift
@@ -21,14 +21,30 @@ struct SharedCardCloudKitService: SharedCardProviding {
let vCardData = card.vCardFilePayload
let expiresAt = Date.now.addingTimeInterval(TimeInterval(ttlDays * 24 * 60 * 60))
let recordID = CKRecord.ID(recordName: UUID().uuidString)
+ var photoAssetFileURL: URL?
let sharedCard = SharedCardRecord(
recordID: recordID,
+ displayName: card.vCardName.isEmpty ? card.fullName : card.vCardName,
+ role: card.role,
+ company: card.company,
vCardData: vCardData,
expiresAt: expiresAt
)
- let record = sharedCard.toCKRecord()
+ do {
+ photoAssetFileURL = try makePhotoAssetFile(from: card.photoData)
+ } catch {
+ throw SharedCardError.uploadFailed(error)
+ }
+
+ defer {
+ if let photoAssetFileURL {
+ try? FileManager.default.removeItem(at: photoAssetFileURL)
+ }
+ }
+
+ let record = sharedCard.toCKRecord(photoAssetFileURL: photoAssetFileURL)
do {
_ = try await database.save(record)
@@ -67,6 +83,13 @@ struct SharedCardCloudKitService: SharedCardProviding {
// Cleanup is best-effort; log but don't throw
}
}
+
+ private func makePhotoAssetFile(from photoData: Data?) throws -> URL? {
+ guard let photoData, !photoData.isEmpty else { return nil }
+ let fileURL = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).jpg")
+ try photoData.write(to: fileURL, options: .atomic)
+ return fileURL
+ }
}
// MARK: - Error Types
diff --git a/BusinessCard/Views/CardsHomeView.swift b/BusinessCard/Views/CardsHomeView.swift
index 5b179eb..d912e8b 100644
--- a/BusinessCard/Views/CardsHomeView.swift
+++ b/BusinessCard/Views/CardsHomeView.swift
@@ -13,13 +13,8 @@ struct CardsHomeView: View {
NavigationStack {
ZStack {
- // Background gradient
- LinearGradient(
- colors: [Color.AppBackground.base, Color.AppBackground.accent],
- startPoint: .top,
- endPoint: .bottom
- )
- .ignoresSafeArea()
+ Color.AppBackground.base
+ .ignoresSafeArea()
if cardStore.cards.isEmpty {
EmptyCardsView(onCreateCard: { showingCreateCard = true })
diff --git a/BusinessCard/Views/SettingsView.swift b/BusinessCard/Views/SettingsView.swift
index 9e97737..1ba79a7 100644
--- a/BusinessCard/Views/SettingsView.swift
+++ b/BusinessCard/Views/SettingsView.swift
@@ -51,8 +51,8 @@ struct SettingsView: View {
)
SettingsCard(
- backgroundColor: Color.AppBackground.secondary,
- borderColor: .clear
+ backgroundColor: Color.AppBackground.elevated,
+ borderColor: AppBorder.standard
) {
SettingsSegmentedPicker(
title: "Theme",
@@ -83,8 +83,8 @@ struct SettingsView: View {
)
SettingsCard(
- backgroundColor: Color.AppBackground.secondary,
- borderColor: .clear
+ backgroundColor: Color.AppBackground.elevated,
+ borderColor: AppBorder.standard
) {
SettingsCardRow {
HStack {
@@ -154,8 +154,8 @@ struct SettingsView: View {
)
SettingsCard(
- backgroundColor: Color.AppBackground.secondary,
- borderColor: .clear
+ backgroundColor: Color.AppBackground.elevated,
+ borderColor: AppBorder.standard
) {
SettingsToggle(
title: "Enable Debug Premium",
diff --git a/BusinessCard/Views/Sheets/ContactFieldEditorSheet.swift b/BusinessCard/Views/Sheets/ContactFieldEditorSheet.swift
index 42f0cd0..537dfc7 100644
--- a/BusinessCard/Views/Sheets/ContactFieldEditorSheet.swift
+++ b/BusinessCard/Views/Sheets/ContactFieldEditorSheet.swift
@@ -5,6 +5,11 @@ import Bedrock
struct ContactFieldEditorSheet: View {
@Environment(\.dismiss) private var dismiss
+ private enum ActiveField: Hashable {
+ case value
+ case title
+ }
+
let fieldType: ContactFieldType
let initialValue: String
let initialTitle: String
@@ -15,6 +20,7 @@ struct ContactFieldEditorSheet: View {
@State private var value: String
@State private var title: String
@State private var postalAddress: PostalAddress
+ @FocusState private var activeField: ActiveField?
init(
fieldType: ContactFieldType,
@@ -101,6 +107,7 @@ struct ContactFieldEditorSheet: View {
.foregroundStyle(Color.Text.primary)
TextField(fieldType.valuePlaceholder, text: $value)
+ .focused($activeField, equals: .value)
.keyboardType(fieldType.keyboardType)
.textInputAutocapitalization(fieldType.autocapitalization)
.textContentType(textContentType)
@@ -143,6 +150,7 @@ struct ContactFieldEditorSheet: View {
.foregroundStyle(Color.Text.primary)
TextField(String(localized: "e.g. Work, Personal"), text: $title)
+ .focused($activeField, equals: .title)
Divider()
@@ -186,6 +194,10 @@ struct ContactFieldEditorSheet: View {
}
}
.padding(Design.Spacing.large)
+ .contentShape(.rect)
+ .onTapGesture {
+ activeField = nil
+ }
}
.background(Color.AppBackground.base)
.navigationTitle(isEditing ? "Edit \(fieldType.displayName)" : "Add \(fieldType.displayName)")
diff --git a/BusinessCardClip/BusinessCardClip.entitlements b/BusinessCardClip/BusinessCardClip.entitlements
index 1d8b984..e23bfd2 100644
--- a/BusinessCardClip/BusinessCardClip.entitlements
+++ b/BusinessCardClip/BusinessCardClip.entitlements
@@ -6,6 +6,14 @@
appclips:$(APPCLIP_DOMAIN)
+ com.apple.developer.icloud-container-identifiers
+
+ $(CLOUDKIT_CONTAINER_IDENTIFIER)
+
+ com.apple.developer.icloud-services
+
+ CloudKit
+
com.apple.developer.parent-application-identifiers
$(AppIdentifierPrefix)$(APP_BUNDLE_IDENTIFIER)
diff --git a/BusinessCardClip/Models/SharedCardSnapshot.swift b/BusinessCardClip/Models/SharedCardSnapshot.swift
index 39a7f24..ca8a159 100644
--- a/BusinessCardClip/Models/SharedCardSnapshot.swift
+++ b/BusinessCardClip/Models/SharedCardSnapshot.swift
@@ -9,17 +9,30 @@ struct SharedCardSnapshot: Sendable {
let company: String
let photoData: Data?
- init(recordName: String, vCardData: String) {
+ init(
+ recordName: String,
+ vCardData: String,
+ displayName: String? = nil,
+ role: String? = nil,
+ company: String? = nil,
+ photoData: Data? = nil
+ ) {
self.recordName = recordName
self.vCardData = vCardData
// Parse display fields from vCard
let lines = vCardData.components(separatedBy: .newlines)
- self.displayName = Self.parseField("FN:", from: lines) ?? "Contact"
- self.role = Self.parseField("TITLE:", from: lines) ?? ""
- self.company = Self.parseField("ORG:", from: lines)?
+ let parsedDisplayName = Self.parseField("FN:", from: lines) ?? "Contact"
+ let parsedRole = Self.parseField("TITLE:", from: lines) ?? ""
+ let parsedCompany = Self.parseField("ORG:", from: lines)?
.components(separatedBy: ";").first ?? ""
- self.photoData = Self.parsePhoto(from: lines)
+ let parsedPhotoData = Self.parsePhoto(from: lines)
+
+ let cleanedDisplayName = displayName?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
+ self.displayName = cleanedDisplayName.isEmpty ? parsedDisplayName : cleanedDisplayName
+ self.role = role ?? parsedRole
+ self.company = company ?? parsedCompany
+ self.photoData = photoData ?? parsedPhotoData
}
private static func parseField(_ prefix: String, from lines: [String]) -> String? {
diff --git a/BusinessCardClip/Services/ClipCloudKitService.swift b/BusinessCardClip/Services/ClipCloudKitService.swift
index 8df07cb..eb18429 100644
--- a/BusinessCardClip/Services/ClipCloudKitService.swift
+++ b/BusinessCardClip/Services/ClipCloudKitService.swift
@@ -29,6 +29,33 @@ struct ClipCloudKitService: Sendable {
throw ClipError.expired
}
- return SharedCardSnapshot(recordName: recordName, vCardData: vCardData)
+ let displayName = record["displayName"] as? String
+ let role = record["role"] as? String
+ let company = record["company"] as? String
+ let photoData = dataFromPhotoAsset(record["photoAsset"] as? CKAsset)
+
+ return SharedCardSnapshot(
+ recordName: recordName,
+ vCardData: vCardData,
+ displayName: displayName,
+ role: role,
+ company: company,
+ photoData: photoData
+ )
+ }
+
+ /// Deletes a shared card from CloudKit after it is saved to Contacts.
+ func deleteSharedCard(recordName: String) async throws {
+ let recordID = CKRecord.ID(recordName: recordName)
+ do {
+ _ = try await database.deleteRecord(withID: recordID)
+ } catch {
+ throw ClipError.deleteFailed
+ }
+ }
+
+ private func dataFromPhotoAsset(_ asset: CKAsset?) -> Data? {
+ guard let fileURL = asset?.fileURL else { return nil }
+ return try? Data(contentsOf: fileURL)
}
}
diff --git a/BusinessCardClip/Services/ClipError.swift b/BusinessCardClip/Services/ClipError.swift
index e3707a9..c0a99ff 100644
--- a/BusinessCardClip/Services/ClipError.swift
+++ b/BusinessCardClip/Services/ClipError.swift
@@ -5,6 +5,7 @@ enum ClipError: Error, LocalizedError {
case fetchFailed
case invalidRecord
case expired
+ case deleteFailed
case contactSaveFailed
case contactsAccessDenied
@@ -16,6 +17,8 @@ enum ClipError: Error, LocalizedError {
return String(localized: "Invalid card data")
case .expired:
return String(localized: "This card has expired")
+ case .deleteFailed:
+ return String(localized: "Could not remove shared card")
case .contactSaveFailed:
return String(localized: "Failed to save contact")
case .contactsAccessDenied:
diff --git a/BusinessCardClip/State/ClipCardStore.swift b/BusinessCardClip/State/ClipCardStore.swift
index f8a5adb..b847d84 100644
--- a/BusinessCardClip/State/ClipCardStore.swift
+++ b/BusinessCardClip/State/ClipCardStore.swift
@@ -48,6 +48,8 @@ final class ClipCardStore {
guard let snapshot else { return }
do {
try await contactSave.saveContact(vCardData: snapshot.vCardData)
+ // Best-effort cleanup so saved cards do not linger in public CloudKit.
+ try? await cloudKit.deleteSharedCard(recordName: snapshot.recordName)
state = .saved
} catch {
state = .error(error.localizedDescription)