From af64f0a7d925af6f1a3a190f377edef2f26bffc6 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Sat, 10 Jan 2026 14:11:33 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- BusinessCard/Models/BusinessCard.swift | 47 +++++----- BusinessCard/Services/ShareLinkService.swift | 8 +- BusinessCard/Services/VCardFileService.swift | 2 +- .../Services/WatchConnectivityService.swift | 12 ++- BusinessCard/State/CardStore.swift | 2 +- BusinessCard/Views/BusinessCardView.swift | 14 +-- BusinessCard/Views/CardEditorView.swift | 27 ++---- BusinessCard/Views/ShareCardView.swift | 2 +- BusinessCard/Views/WidgetsView.swift | 2 +- BusinessCardTests/BusinessCardTests.swift | 18 ++-- .../Design/WatchDesignConstants.swift | 1 + .../Models/WatchCard.swift | 4 +- .../Resources/Localizable.xcstrings | 12 ++- .../Services/WatchConnectivityService.swift | 4 +- .../State/WatchCardStore.swift | 69 +++----------- .../Views/WatchContentView.swift | 89 +++++-------------- 16 files changed, 104 insertions(+), 209 deletions(-) diff --git a/BusinessCard/Models/BusinessCard.swift b/BusinessCard/Models/BusinessCard.swift index d12091b..a47e99e 100644 --- a/BusinessCard/Models/BusinessCard.swift +++ b/BusinessCard/Models/BusinessCard.swift @@ -5,7 +5,6 @@ import SwiftUI @Model final class BusinessCard { var id: UUID - var displayName: String var role: String var company: String var label: String @@ -46,7 +45,6 @@ final class BusinessCard { init( id: UUID = UUID(), - displayName: String = "", role: String = "", company: String = "", label: String = "Work", @@ -74,7 +72,6 @@ final class BusinessCard { logoData: Data? = nil ) { self.id = id - self.displayName = displayName self.role = role self.company = company self.label = label @@ -197,13 +194,12 @@ final class BusinessCard { } } - /// Computed display name with special formatting: - /// - Preferred name in quotes: "Bubba" - /// - Maiden name in parentheses: (Hackney) - /// - Pronouns in parentheses: (He/Him) - var computedDisplayName: String { - if !displayName.isEmpty { return displayName } - + // MARK: - Name Properties (Single Source of Truth) + + /// The full formatted name - THIS IS THE SINGLE SOURCE OF TRUTH for display. + /// Always computed from individual name fields. + /// Includes special formatting: preferred name in quotes, maiden name and pronouns in parentheses. + var fullName: String { var parts: [String] = [] if !prefix.isEmpty { parts.append(prefix) } @@ -218,18 +214,12 @@ final class BusinessCard { return parts.joined(separator: " ") } - /// Returns the simple name for display (without formatting) - used for vCard - var simpleName: String { - if !displayName.isEmpty { return displayName } - let parts = [prefix, firstName, middleName, lastName, suffix].filter { !$0.isEmpty } - return parts.joined(separator: " ") - } - - /// Returns the name to display (preferredName or first/last name) - var effectiveDisplayName: String { - if !preferredName.isEmpty { return preferredName } - let parts = [firstName, lastName].filter { !$0.isEmpty } - return parts.isEmpty ? computedDisplayName : parts.joined(separator: " ") + /// Plain name for vCard export (no quotes or parentheses formatting). + /// Used when generating contact cards for sharing. + var vCardName: String { + [prefix, firstName, middleName, lastName, suffix] + .filter { !$0.isEmpty } + .joined(separator: " ") } @MainActor @@ -246,7 +236,7 @@ final class BusinessCard { lines.append("N:\(structuredName)") // FN: Formatted name - let formattedName = simpleName.isEmpty ? displayName : simpleName + let formattedName = vCardName lines.append("FN:\(escapeVCardValue(formattedName))") // NICKNAME: Preferred name @@ -378,7 +368,7 @@ final class BusinessCard { lines.append("N:\(structuredName)") // FN: Formatted name - let formattedName = simpleName.isEmpty ? displayName : simpleName + let formattedName = vCardName lines.append("FN:\(escapeVCardValue(formattedName))") // PHOTO: Embedded profile photo as base64-encoded JPEG @@ -513,7 +503,6 @@ extension BusinessCard { // Sample 1: Property Developer - Uses coverWithAvatarAndLogo layout // Best when: Has cover, logo, and profile - logo centered on cover let sample1 = BusinessCard( - displayName: "Daniel Sullivan", role: "Property Developer", company: "WR Construction", label: "Work", @@ -522,6 +511,8 @@ extension BusinessCard { layoutStyleRawValue: "split", headerLayoutRawValue: "coverWithAvatarAndLogo", avatarSystemName: "person.crop.circle", + firstName: "Daniel", + lastName: "Sullivan", pronouns: "he/him", bio: "Building the future of Dallas real estate" ) @@ -535,7 +526,6 @@ extension BusinessCard { // Sample 2: Creative Lead - Uses logoBanner layout // Best when: Strong logo, no cover needed let sample2 = BusinessCard( - displayName: "Maya Chen", role: "Creative Lead", company: "Signal Studio", label: "Creative", @@ -544,6 +534,8 @@ extension BusinessCard { layoutStyleRawValue: "stacked", headerLayoutRawValue: "logoBanner", avatarSystemName: "sparkles", + firstName: "Maya", + lastName: "Chen", pronouns: "she/her", bio: "Designing experiences that matter" ) @@ -558,7 +550,6 @@ extension BusinessCard { // Sample 3: DJ - Uses profileBanner layout // Best when: Strong profile photo, personal brand let sample3 = BusinessCard( - displayName: "DJ Michaels", role: "DJ", company: "Live Sessions", label: "Music", @@ -567,6 +558,8 @@ extension BusinessCard { layoutStyleRawValue: "photo", headerLayoutRawValue: "profileBanner", avatarSystemName: "music.mic", + firstName: "DJ", + lastName: "Michaels", bio: "Bringing the beats to your events" ) context.insert(sample3) diff --git a/BusinessCard/Services/ShareLinkService.swift b/BusinessCard/Services/ShareLinkService.swift index 445506a..1ab5731 100644 --- a/BusinessCard/Services/ShareLinkService.swift +++ b/BusinessCard/Services/ShareLinkService.swift @@ -6,21 +6,21 @@ struct ShareLinkService: ShareLinkProviding { } func smsURL(for card: BusinessCard) -> URL { - let body = String.localized("ShareTextBody", card.displayName, card.shareURL.absoluteString) + let body = String.localized("ShareTextBody", card.fullName, card.shareURL.absoluteString) let query = body.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" return URL(string: "sms:&body=\(query)") ?? card.shareURL } func emailURL(for card: BusinessCard) -> URL { - let subject = String.localized("ShareEmailSubject", card.displayName) - let body = String.localized("ShareEmailBody", card.displayName, card.shareURL.absoluteString) + let subject = String.localized("ShareEmailSubject", card.fullName) + let body = String.localized("ShareEmailBody", card.fullName, card.shareURL.absoluteString) let subjectQuery = subject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" let bodyQuery = body.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" return URL(string: "mailto:?subject=\(subjectQuery)&body=\(bodyQuery)") ?? card.shareURL } func whatsappURL(for card: BusinessCard) -> URL { - let message = String.localized("ShareWhatsAppBody", card.displayName, card.shareURL.absoluteString) + let message = String.localized("ShareWhatsAppBody", card.fullName, card.shareURL.absoluteString) let query = message.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" return URL(string: "https://wa.me/?text=\(query)") ?? card.shareURL } diff --git a/BusinessCard/Services/VCardFileService.swift b/BusinessCard/Services/VCardFileService.swift index 7be5c1e..b497fc6 100644 --- a/BusinessCard/Services/VCardFileService.swift +++ b/BusinessCard/Services/VCardFileService.swift @@ -11,7 +11,7 @@ struct VCardFileService { let payload = card.vCardFilePayload // Create a sanitized filename from the card's name - let sanitizedName = sanitizeFilename(card.simpleName.isEmpty ? card.displayName : card.simpleName) + let sanitizedName = sanitizeFilename(card.vCardName) let filename = sanitizedName.isEmpty ? "contact" : sanitizedName // Write to temp directory with .vcf extension diff --git a/BusinessCard/Services/WatchConnectivityService.swift b/BusinessCard/Services/WatchConnectivityService.swift index c6dc08b..cbff843 100644 --- a/BusinessCard/Services/WatchConnectivityService.swift +++ b/BusinessCard/Services/WatchConnectivityService.swift @@ -131,9 +131,9 @@ final class WatchConnectivityService: NSObject { let addressValue = card.firstContactField(ofType: "address")?.value ?? "" let location = PostalAddress.decode(from: addressValue)?.singleLineString ?? addressValue - // Build vCard payload for QR code generation + // Build vCard payload for QR code generation (use vCardName for compatibility) let vCardPayload = buildVCardPayload( - displayName: card.displayName, + displayName: card.vCardName, company: card.company, role: card.role, phone: phone, @@ -149,9 +149,13 @@ final class WatchConnectivityService: NSObject { // Generate QR code image data on iOS (CoreImage not available on watchOS) let qrImageData = generateQRCodePNGData(from: vCardPayload) + // Use fullName - the single source of truth for display names + let syncDisplayName = card.fullName + Design.debugLog("WatchConnectivity: Syncing card '\(syncDisplayName)'") + return SyncableCard( id: card.id, - displayName: card.displayName, + fullName: syncDisplayName, role: card.role, company: card.company, email: email, @@ -269,7 +273,7 @@ extension WatchConnectivityService: WCSessionDelegate { /// A simplified card structure that can be shared between iOS and watchOS struct SyncableCard: Codable, Identifiable { let id: UUID - var displayName: String + var fullName: String var role: String var company: String var email: String diff --git a/BusinessCard/State/CardStore.swift b/BusinessCard/State/CardStore.swift index c33d0e5..79f4dee 100644 --- a/BusinessCard/State/CardStore.swift +++ b/BusinessCard/State/CardStore.swift @@ -42,7 +42,7 @@ final class CardStore: BusinessCardProviding { } func updateCard(_ card: BusinessCard) { - Design.debugLog("CardStore: updateCard called for: \(card.displayName)") + Design.debugLog("CardStore: updateCard - fullName: '\(card.fullName)'") card.updatedAt = .now saveContext() fetchCards() diff --git a/BusinessCard/Views/BusinessCardView.swift b/BusinessCard/Views/BusinessCardView.swift index 76c0bd9..9952cb4 100644 --- a/BusinessCard/Views/BusinessCardView.swift +++ b/BusinessCard/Views/BusinessCardView.swift @@ -28,7 +28,7 @@ struct BusinessCardView: View { ) .accessibilityElement(children: .ignore) .accessibilityLabel(String.localized("Business card")) - .accessibilityValue("\(card.effectiveDisplayName), \(card.role), \(card.company)") + .accessibilityValue("\(card.fullName), \(card.role), \(card.company)") } } @@ -172,7 +172,7 @@ private struct CardContentView: View { VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { HStack(spacing: Design.Spacing.xSmall) { - Text(card.effectiveDisplayName) + Text(card.fullName) .font(.title2) .bold() .foregroundStyle(textColor) @@ -390,11 +390,12 @@ private struct ContactFieldRowView: View { #Preview("Profile Banner") { @Previewable @State var card = BusinessCard( - displayName: "Matt Bruce", role: "Lead iOS Developer", company: "Toyota", themeName: "Coral", - headerLayoutRawValue: "profileBanner" + headerLayoutRawValue: "profileBanner", + firstName: "Matt", + lastName: "Bruce" ) BusinessCardView(card: card) @@ -404,11 +405,12 @@ private struct ContactFieldRowView: View { #Preview("Cover + Avatar + Logo") { @Previewable @State var card = BusinessCard( - displayName: "Matt Bruce", role: "Lead iOS Developer", company: "Toyota", themeName: "Violet", - headerLayoutRawValue: "coverWithAvatarAndLogo" + headerLayoutRawValue: "coverWithAvatarAndLogo", + firstName: "Matt", + lastName: "Bruce" ) BusinessCardView(card: card) diff --git a/BusinessCard/Views/CardEditorView.swift b/BusinessCard/Views/CardEditorView.swift index 50a840d..2a9f2dd 100644 --- a/BusinessCard/Views/CardEditorView.swift +++ b/BusinessCard/Views/CardEditorView.swift @@ -11,7 +11,6 @@ struct CardEditorView: View { let onSave: (BusinessCard) -> Void // Name fields - @State private var displayName = "" @State private var prefix = "" @State private var firstName = "" @State private var middleName = "" @@ -86,20 +85,12 @@ struct CardEditorView: View { private var isEditing: Bool { card != nil } private var isFormValid: Bool { - !effectiveDisplayName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + !fullName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } - /// Simple name for validation and storage (without quotes/parentheses formatting) - private var effectiveDisplayName: String { - if !displayName.isEmpty { return displayName } - let parts = [prefix, firstName, middleName, lastName, suffix].filter { !$0.isEmpty } - return parts.joined(separator: " ") - } - - /// Formatted name for display with special formatting - private var formattedDisplayName: String { - if !displayName.isEmpty { return displayName } - + /// The full formatted name - matches BusinessCard.fullName logic. + /// Single source of truth for name display in the editor. + private var fullName: String { var parts: [String] = [] if !prefix.isEmpty { parts.append(prefix) } @@ -158,8 +149,8 @@ struct CardEditorView: View { withAnimation { showNameDetails.toggle() } } label: { HStack { - Text(formattedDisplayName.isEmpty ? "Full Name" : formattedDisplayName) - .foregroundStyle(formattedDisplayName.isEmpty ? Color.secondary : Color.primary) + Text(fullName.isEmpty ? "Full Name" : fullName) + .foregroundStyle(fullName.isEmpty ? Color.secondary : Color.primary) Spacer() Image(systemName: showNameDetails ? "chevron.up" : "chevron.down") .foregroundStyle(Color.accentColor) @@ -316,7 +307,7 @@ struct CardEditorView: View { logoData: logoData, avatarSystemName: avatarSystemName, theme: selectedTheme, - displayName: effectiveDisplayName, + displayName: fullName, role: role, company: company ) @@ -1103,7 +1094,6 @@ private struct CardPreviewSheet: View { private extension CardEditorView { func loadCardData() { guard let card else { return } - displayName = card.displayName prefix = card.prefix firstName = card.firstName middleName = card.middleName @@ -1182,7 +1172,6 @@ private extension CardEditorView { } func updateCard(_ card: BusinessCard) { - card.displayName = displayName.isEmpty ? effectiveDisplayName : displayName card.prefix = prefix card.firstName = firstName card.middleName = middleName @@ -1212,7 +1201,6 @@ private extension CardEditorView { func createCard() -> BusinessCard { let newCard = BusinessCard( - displayName: displayName.isEmpty ? effectiveDisplayName : displayName, role: role, company: company, label: label, @@ -1246,7 +1234,6 @@ private extension CardEditorView { func buildPreviewCard() -> BusinessCard { let previewCard = BusinessCard( - displayName: displayName.isEmpty ? effectiveDisplayName : displayName, role: role, company: company, label: label, diff --git a/BusinessCard/Views/ShareCardView.swift b/BusinessCard/Views/ShareCardView.swift index 00aaa64..88876a9 100644 --- a/BusinessCard/Views/ShareCardView.swift +++ b/BusinessCard/Views/ShareCardView.swift @@ -67,7 +67,7 @@ struct ShareCardView: View { .ignoresSafeArea() } .sheet(item: $mailComposeURL) { url in - MailComposeView(vCardURL: url, cardName: appState.cardStore.selectedCard?.simpleName ?? "Contact") + MailComposeView(vCardURL: url, cardName: appState.cardStore.selectedCard?.vCardName ?? "Contact") .ignoresSafeArea() } .onAppear { diff --git a/BusinessCard/Views/WidgetsView.swift b/BusinessCard/Views/WidgetsView.swift index fca1dc7..477b19e 100644 --- a/BusinessCard/Views/WidgetsView.swift +++ b/BusinessCard/Views/WidgetsView.swift @@ -53,7 +53,7 @@ private struct PhoneWidgetPreview: View { .frame(width: Design.CardSize.widgetPhoneHeight, height: Design.CardSize.widgetPhoneHeight) VStack(alignment: .leading, spacing: Design.Spacing.xSmall) { - Text(card.displayName) + Text(card.fullName) .font(.headline) .foregroundStyle(Color.Text.primary) Text(card.role) diff --git a/BusinessCardTests/BusinessCardTests.swift b/BusinessCardTests/BusinessCardTests.swift index ba49e5d..6a2d167 100644 --- a/BusinessCardTests/BusinessCardTests.swift +++ b/BusinessCardTests/BusinessCardTests.swift @@ -95,8 +95,8 @@ struct BusinessCardTests { let context = container.mainContext // Insert cards directly instead of using samples (which might trigger other logic) - let card1 = BusinessCard(displayName: "Card One", role: "Role", company: "Company", isDefault: true) - let card2 = BusinessCard(displayName: "Card Two", role: "Role", company: "Company", isDefault: false) + let card1 = BusinessCard(role: "Role", company: "Company", isDefault: true, firstName: "Card", lastName: "One") + let card2 = BusinessCard(role: "Role", company: "Company", isDefault: false, firstName: "Card", lastName: "Two") context.insert(card1) context.insert(card2) try context.save() @@ -143,9 +143,10 @@ struct BusinessCardTests { let initialCount = store.cards.count let newCard = BusinessCard( - displayName: "New User", role: "Manager", - company: "New Corp" + company: "New Corp", + firstName: "New", + lastName: "User" ) store.addCard(newCard) @@ -156,8 +157,8 @@ struct BusinessCardTests { let container = try makeTestContainer() let context = container.mainContext - let card1 = BusinessCard(displayName: "Card One", role: "Role", company: "Company") - let card2 = BusinessCard(displayName: "Card Two", role: "Role", company: "Company") + let card1 = BusinessCard(role: "Role", company: "Company", firstName: "Card", lastName: "One") + let card2 = BusinessCard(role: "Role", company: "Company", firstName: "Card", lastName: "Two") context.insert(card1) context.insert(card2) try context.save() @@ -185,12 +186,13 @@ struct BusinessCardTests { let store = CardStore(modelContext: context) - card.displayName = "Updated Name" + card.firstName = "Updated" + card.lastName = "Name" card.role = "Updated Role" store.updateCard(card) let updatedCard = store.cards.first(where: { $0.id == card.id }) - #expect(updatedCard?.displayName == "Updated Name") + #expect(updatedCard?.fullName == "Updated Name") #expect(updatedCard?.role == "Updated Role") } diff --git a/BusinessCardWatch Watch App/Design/WatchDesignConstants.swift b/BusinessCardWatch Watch App/Design/WatchDesignConstants.swift index 5be895b..f1f0a22 100644 --- a/BusinessCardWatch Watch App/Design/WatchDesignConstants.swift +++ b/BusinessCardWatch Watch App/Design/WatchDesignConstants.swift @@ -9,6 +9,7 @@ enum WatchDesign { } enum Spacing { + static let extraSmall: CGFloat = 4 static let small: CGFloat = 6 static let medium: CGFloat = 10 static let large: CGFloat = 16 diff --git a/BusinessCardWatch Watch App/Models/WatchCard.swift b/BusinessCardWatch Watch App/Models/WatchCard.swift index e029315..37bca2d 100644 --- a/BusinessCardWatch Watch App/Models/WatchCard.swift +++ b/BusinessCardWatch Watch App/Models/WatchCard.swift @@ -1,10 +1,10 @@ import Foundation import SwiftUI -/// A simplified card structure synced from the iOS app via App Group UserDefaults +/// A simplified card structure synced from the iOS app via WatchConnectivity struct WatchCard: Codable, Identifiable, Hashable { let id: UUID - var displayName: String + var fullName: String var role: String var company: String var email: String diff --git a/BusinessCardWatch Watch App/Resources/Localizable.xcstrings b/BusinessCardWatch Watch App/Resources/Localizable.xcstrings index c053c26..866fbfe 100644 --- a/BusinessCardWatch Watch App/Resources/Localizable.xcstrings +++ b/BusinessCardWatch Watch App/Resources/Localizable.xcstrings @@ -14,6 +14,7 @@ } }, "Choose default" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -36,6 +37,7 @@ } }, "Default Card" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -57,15 +59,12 @@ } } }, - "Default card QR code" : { - "comment" : "An accessibility label describing a view that shows the QR code of a user's default watch card.", - "isCommentAutoGenerated" : true - }, "No Cards" : { "comment" : "A message displayed when a user has no watch cards.", "isCommentAutoGenerated" : true }, "Not selected" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -91,7 +90,12 @@ "comment" : "A description of the action to create a watch card from the iPhone app.", "isCommentAutoGenerated" : true }, + "QR code" : { + "comment" : "A label describing a view that displays a QR code.", + "isCommentAutoGenerated" : true + }, "Selected" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { diff --git a/BusinessCardWatch Watch App/Services/WatchConnectivityService.swift b/BusinessCardWatch Watch App/Services/WatchConnectivityService.swift index 9f89503..5e5b99f 100644 --- a/BusinessCardWatch Watch App/Services/WatchConnectivityService.swift +++ b/BusinessCardWatch Watch App/Services/WatchConnectivityService.swift @@ -49,7 +49,7 @@ final class WatchConnectivityService: NSObject { let watchCards = syncableCards.map { syncable in WatchCard( id: syncable.id, - displayName: syncable.displayName, + fullName: syncable.fullName, role: syncable.role, company: syncable.company, email: syncable.email, @@ -108,7 +108,7 @@ extension WatchConnectivityService: WCSessionDelegate { /// Syncable card structure matching iOS side (for decoding) private struct SyncableCard: Codable, Identifiable { let id: UUID - var displayName: String + var fullName: String var role: String var company: String var email: String diff --git a/BusinessCardWatch Watch App/State/WatchCardStore.swift b/BusinessCardWatch Watch App/State/WatchCardStore.swift index 05d04aa..3ac7fc8 100644 --- a/BusinessCardWatch Watch App/State/WatchCardStore.swift +++ b/BusinessCardWatch Watch App/State/WatchCardStore.swift @@ -4,56 +4,26 @@ import Observation @Observable @MainActor final class WatchCardStore { - private static let defaultCardIDKey = "WatchDefaultCardID" - private(set) var cards: [WatchCard] = [] - var defaultCardID: UUID? { - didSet { - persistDefaultID() - } - } + + /// The ID of the default card (synced from iPhone) + private(set) var defaultCardID: UUID? init() { - loadDefaultID() WatchDesign.debugLog("WatchCardStore: Initialized, waiting for cards from iPhone") // Set up callback for when cards are received from iPhone WatchConnectivityService.shared.onCardsReceived = { [weak self] cards in self?.updateCards(cards) } - - #if targetEnvironment(simulator) - // Load sample data on simulator since WatchConnectivity often doesn't work - if cards.isEmpty { - loadSimulatorSampleData() - } - #endif } - - #if targetEnvironment(simulator) - private func loadSimulatorSampleData() { - WatchDesign.debugLog("WatchCardStore: Loading simulator sample data") - cards = [ - WatchCard( - id: UUID(), - displayName: "Test User", - role: "iOS Developer", - company: "Sample Corp", - email: "test@example.com", - phone: "+1 555-0123", - website: "example.com", - location: "San Francisco, CA", - isDefault: true, - qrCodeImageData: nil - ) - ] - defaultCardID = cards.first?.id - } - #endif + /// The default card to display (always synced from iPhone) var defaultCard: WatchCard? { - guard let defaultCardID else { return cards.first(where: { $0.isDefault }) ?? cards.first } - return cards.first(where: { $0.id == defaultCardID }) ?? cards.first(where: { $0.isDefault }) ?? cards.first + if let defaultCardID { + return cards.first { $0.id == defaultCardID } + } + return cards.first { $0.isDefault } ?? cards.first } /// Called by WatchConnectivityService when cards are received from iPhone @@ -61,26 +31,7 @@ final class WatchCardStore { WatchDesign.debugLog("WatchCardStore: Received \(newCards.count) cards from iPhone") cards = newCards - // Update default card ID if current selection is no longer valid - if let currentDefault = defaultCardID, !cards.contains(where: { $0.id == currentDefault }) { - defaultCardID = cards.first(where: { $0.isDefault })?.id ?? cards.first?.id - } else if defaultCardID == nil { - defaultCardID = cards.first(where: { $0.isDefault })?.id ?? cards.first?.id - } - } - - func setDefault(_ card: WatchCard) { - defaultCardID = card.id - } - - private func persistDefaultID() { - UserDefaults.standard.set(defaultCardID?.uuidString ?? "", forKey: Self.defaultCardIDKey) - } - - private func loadDefaultID() { - let storedValue = UserDefaults.standard.string(forKey: Self.defaultCardIDKey) ?? "" - if let id = UUID(uuidString: storedValue) { - defaultCardID = id - } + // Always use the iPhone's default card - the watch no longer has its own picker + defaultCardID = cards.first { $0.isDefault }?.id ?? cards.first?.id } } diff --git a/BusinessCardWatch Watch App/Views/WatchContentView.swift b/BusinessCardWatch Watch App/Views/WatchContentView.swift index 08dfa12..064b11c 100644 --- a/BusinessCardWatch Watch App/Views/WatchContentView.swift +++ b/BusinessCardWatch Watch App/Views/WatchContentView.swift @@ -4,21 +4,11 @@ struct WatchContentView: View { @Environment(WatchCardStore.self) private var cardStore var body: some View { - ScrollView { - VStack(spacing: WatchDesign.Spacing.large) { - if let card = cardStore.defaultCard { - WatchQRCodeCardView(card: card) - } else if cardStore.cards.isEmpty { - WatchEmptyStateView() - } - - if !cardStore.cards.isEmpty { - WatchCardPickerView() - } - } - .padding(WatchDesign.Spacing.medium) + if let card = cardStore.defaultCard { + WatchQRCodeCardView(card: card) + } else { + WatchEmptyStateView() } - .background(Color.WatchPalette.background) } } @@ -46,11 +36,7 @@ private struct WatchQRCodeCardView: View { let card: WatchCard var body: some View { - VStack(spacing: WatchDesign.Spacing.small) { - Text("Default Card") - .font(.headline) - .foregroundStyle(Color.WatchPalette.text) - + VStack(spacing: WatchDesign.Spacing.medium) { if let image = card.qrCodeImage { image .resizable() @@ -71,61 +57,26 @@ private struct WatchQRCodeCardView: View { .foregroundStyle(Color.WatchPalette.muted) } .frame(width: WatchDesign.Size.qrSize, height: WatchDesign.Size.qrSize) - .background(Color.WatchPalette.card) - .clipShape(.rect(cornerRadius: WatchDesign.CornerRadius.large)) + .background(Color.WatchPalette.card) + .clipShape(.rect(cornerRadius: WatchDesign.CornerRadius.large)) } - Text(card.displayName) - .font(.subheadline) - .foregroundStyle(Color.WatchPalette.text) - Text(card.role) - .font(.caption) - .foregroundStyle(Color.WatchPalette.muted) + VStack(spacing: WatchDesign.Spacing.extraSmall) { + Text(card.fullName) + .font(.headline) + .foregroundStyle(Color.WatchPalette.text) + + Text(card.role) + .font(.caption) + .foregroundStyle(Color.WatchPalette.muted) + } } .padding(WatchDesign.Spacing.medium) - .background(Color.WatchPalette.card) - .clipShape(.rect(cornerRadius: WatchDesign.CornerRadius.large)) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.WatchPalette.background) .accessibilityElement(children: .ignore) - .accessibilityLabel(String(localized: "Default card QR code")) - .accessibilityValue("\(card.displayName), \(card.role)") - } -} - -private struct WatchCardPickerView: View { - @Environment(WatchCardStore.self) private var cardStore - - var body: some View { - VStack(alignment: .leading, spacing: WatchDesign.Spacing.small) { - Text("Choose default") - .font(.headline) - .foregroundStyle(Color.WatchPalette.text) - - ForEach(cardStore.cards) { card in - Button { - cardStore.setDefault(card) - } label: { - HStack { - Text(card.displayName) - .foregroundStyle(Color.WatchPalette.text) - Spacer() - if card.id == cardStore.defaultCardID { - Image(systemName: "checkmark") - .foregroundStyle(Color.WatchPalette.accent) - } - } - } - .buttonStyle(.plain) - .padding(.vertical, WatchDesign.Spacing.small) - .padding(.horizontal, WatchDesign.Spacing.medium) - .frame(maxWidth: .infinity, alignment: .leading) - .background(card.id == cardStore.defaultCardID ? Color.WatchPalette.accent.opacity(WatchDesign.Opacity.strong) : Color.WatchPalette.card) - .clipShape(.rect(cornerRadius: WatchDesign.CornerRadius.medium)) - .accessibilityValue(card.id == cardStore.defaultCardID ? String(localized: "Selected") : String(localized: "Not selected")) - } - } - .padding(WatchDesign.Spacing.medium) - .background(Color.WatchPalette.card.opacity(WatchDesign.Opacity.hint)) - .clipShape(.rect(cornerRadius: WatchDesign.CornerRadius.large)) + .accessibilityLabel(String(localized: "QR code")) + .accessibilityValue("\(card.fullName), \(card.role)") } }