diff --git a/BusinessCard/Models/ContactField.swift b/BusinessCard/Models/ContactField.swift index 388d48f..27f30ef 100644 --- a/BusinessCard/Models/ContactField.swift +++ b/BusinessCard/Models/ContactField.swift @@ -51,6 +51,14 @@ final class ContactField { fieldType?.displayName ?? typeId } + /// Formatted display value using the field type's formatter + /// For addresses, this returns a multi-line formatted string + /// For other fields, returns the value as-is + @MainActor + var displayValue: String { + fieldType?.formattedDisplayValue(value) ?? value + } + /// Returns an Image view for this field's icon @MainActor func iconImage() -> Image { diff --git a/BusinessCard/Models/ContactFieldType.swift b/BusinessCard/Models/ContactFieldType.swift index 0bbc852..284e4ae 100644 --- a/BusinessCard/Models/ContactFieldType.swift +++ b/BusinessCard/Models/ContactFieldType.swift @@ -39,6 +39,7 @@ struct ContactFieldType: Identifiable, Hashable, Sendable { let keyboardType: UIKeyboardType let autocapitalization: TextInputAutocapitalization let urlBuilder: @Sendable (String) -> URL? + let displayValueFormatter: @Sendable (String) -> String init( id: String, @@ -52,7 +53,8 @@ struct ContactFieldType: Identifiable, Hashable, Sendable { titleSuggestions: [String], keyboardType: UIKeyboardType, autocapitalization: TextInputAutocapitalization = .never, - urlBuilder: @escaping @Sendable (String) -> URL? + urlBuilder: @escaping @Sendable (String) -> URL?, + displayValueFormatter: @escaping @Sendable (String) -> String = { $0 } ) { self.id = id self.displayName = displayName @@ -66,6 +68,7 @@ struct ContactFieldType: Identifiable, Hashable, Sendable { self.keyboardType = keyboardType self.autocapitalization = autocapitalization self.urlBuilder = urlBuilder + self.displayValueFormatter = displayValueFormatter } /// Returns an Image view for this field type's icon @@ -93,6 +96,13 @@ struct ContactFieldType: Identifiable, Hashable, Sendable { func buildURL(value: String) -> URL? { urlBuilder(value) } + + // MARK: - Display Value Formatting + + /// Returns a formatted display value for the given raw value + func formattedDisplayValue(_ value: String) -> String { + displayValueFormatter(value) + } } // MARK: - All Field Types @@ -183,7 +193,8 @@ extension ContactFieldType { urlBuilder: { value in let encoded = value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? value return URL(string: "maps://?q=\(encoded)") - } + }, + displayValueFormatter: formatAddressForDisplay ) // MARK: - Social Media @@ -641,6 +652,26 @@ extension ContactFieldType { ) } +// MARK: - Display Value Formatters + +/// Formats an address for multi-line display +/// Splits on commas and puts each component on a new line +nonisolated private func formatAddressForDisplay(_ value: String) -> String { + let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return value } + + // Split by comma, trim each component, and join with newlines + let components = trimmed + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespaces) } + .filter { !$0.isEmpty } + + // If only one component, return as-is + guard components.count > 1 else { return trimmed } + + return components.joined(separator: "\n") +} + // MARK: - URL Helper Functions nonisolated private func buildWebURL(_ value: String) -> URL? { diff --git a/BusinessCard/Resources/Localizable.xcstrings b/BusinessCard/Resources/Localizable.xcstrings index 20a0dfd..67ddafd 100644 --- a/BusinessCard/Resources/Localizable.xcstrings +++ b/BusinessCard/Resources/Localizable.xcstrings @@ -606,6 +606,9 @@ } } } + }, + "The default card is used for sharing and widgets." : { + }, "This doesn't appear to be a business card QR code." : { diff --git a/BusinessCard/Views/CardEditorView.swift b/BusinessCard/Views/CardEditorView.swift index 4965b9e..cde71ac 100644 --- a/BusinessCard/Views/CardEditorView.swift +++ b/BusinessCard/Views/CardEditorView.swift @@ -44,6 +44,9 @@ struct CardEditorView: View { @State private var coverPhotoData: Data? @State private var logoData: Data? + // Default card + @State private var isDefault = false + // Photo editor state - just one variable! @State private var editingImageType: ImageType? @@ -106,6 +109,15 @@ struct CardEditorView: View { var body: some View { NavigationStack { Form { + // Default card toggle + Section { + Toggle(isOn: $isDefault) { + Label(String.localized("Default Card"), systemImage: "checkmark.seal.fill") + } + } footer: { + Text("The default card is used for sharing and widgets.") + } + // Card Style section Section { CardStylePicker(selectedTheme: $selectedTheme) @@ -651,6 +663,7 @@ private extension CardEditorView { bio = card.bio accreditations = card.accreditations avatarSystemName = card.avatarSystemName + isDefault = card.isDefault // Load contact fields from the array contactFields = card.orderedContactFields.compactMap { $0.toAddedContactField() } @@ -693,9 +706,19 @@ private extension CardEditorView { if let existingCard = card { updateCard(existingCard) onSave(existingCard) + + // Handle default card toggle + if isDefault { + appState.cardStore.setDefaultCard(existingCard) + } } else { let newCard = createCard() onSave(newCard) + + // Handle default card toggle for new cards + if isDefault { + appState.cardStore.setDefaultCard(newCard) + } } dismiss() } diff --git a/BusinessCard/Views/CardsHomeView.swift b/BusinessCard/Views/CardsHomeView.swift index 2d8c57f..5982afe 100644 --- a/BusinessCard/Views/CardsHomeView.swift +++ b/BusinessCard/Views/CardsHomeView.swift @@ -27,12 +27,8 @@ struct CardsHomeView: View { // Full-screen swipeable cards TabView(selection: $cardStore.selectedCardID) { ForEach(cardStore.cards) { card in - CardPageView( - card: card, - isDefault: card.isDefault, - onSetDefault: { cardStore.setDefaultCard(card) } - ) - .tag(Optional(card.id)) + CardPageView(card: card) + .tag(Optional(card.id)) } } .tabViewStyle(.page(indexDisplayMode: .automatic)) @@ -97,23 +93,11 @@ struct CardsHomeView: View { private struct CardPageView: View { let card: BusinessCard - let isDefault: Bool - let onSetDefault: () -> Void var body: some View { ScrollView { VStack(spacing: Design.Spacing.large) { BusinessCardView(card: card) - - // Default card toggle - Button( - isDefault ? String.localized("Default card") : String.localized("Set as default"), - systemImage: isDefault ? "checkmark.seal.fill" : "checkmark.seal", - action: onSetDefault - ) - .buttonStyle(.bordered) - .tint(Color.Accent.red) - .accessibilityHint(String.localized("Sets this card as your default sharing card")) } .padding(.horizontal, Design.Spacing.large) .padding(.vertical, Design.Spacing.xLarge)