Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-01-09 10:37:42 -06:00
parent 8c40e638e5
commit 954f21616e
5 changed files with 69 additions and 20 deletions

View File

@ -51,6 +51,14 @@ final class ContactField {
fieldType?.displayName ?? typeId 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 /// Returns an Image view for this field's icon
@MainActor @MainActor
func iconImage() -> Image { func iconImage() -> Image {

View File

@ -39,6 +39,7 @@ struct ContactFieldType: Identifiable, Hashable, Sendable {
let keyboardType: UIKeyboardType let keyboardType: UIKeyboardType
let autocapitalization: TextInputAutocapitalization let autocapitalization: TextInputAutocapitalization
let urlBuilder: @Sendable (String) -> URL? let urlBuilder: @Sendable (String) -> URL?
let displayValueFormatter: @Sendable (String) -> String
init( init(
id: String, id: String,
@ -52,7 +53,8 @@ struct ContactFieldType: Identifiable, Hashable, Sendable {
titleSuggestions: [String], titleSuggestions: [String],
keyboardType: UIKeyboardType, keyboardType: UIKeyboardType,
autocapitalization: TextInputAutocapitalization = .never, autocapitalization: TextInputAutocapitalization = .never,
urlBuilder: @escaping @Sendable (String) -> URL? urlBuilder: @escaping @Sendable (String) -> URL?,
displayValueFormatter: @escaping @Sendable (String) -> String = { $0 }
) { ) {
self.id = id self.id = id
self.displayName = displayName self.displayName = displayName
@ -66,6 +68,7 @@ struct ContactFieldType: Identifiable, Hashable, Sendable {
self.keyboardType = keyboardType self.keyboardType = keyboardType
self.autocapitalization = autocapitalization self.autocapitalization = autocapitalization
self.urlBuilder = urlBuilder self.urlBuilder = urlBuilder
self.displayValueFormatter = displayValueFormatter
} }
/// Returns an Image view for this field type's icon /// Returns an Image view for this field type's icon
@ -93,6 +96,13 @@ struct ContactFieldType: Identifiable, Hashable, Sendable {
func buildURL(value: String) -> URL? { func buildURL(value: String) -> URL? {
urlBuilder(value) 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 // MARK: - All Field Types
@ -183,7 +193,8 @@ extension ContactFieldType {
urlBuilder: { value in urlBuilder: { value in
let encoded = value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? value let encoded = value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? value
return URL(string: "maps://?q=\(encoded)") return URL(string: "maps://?q=\(encoded)")
} },
displayValueFormatter: formatAddressForDisplay
) )
// MARK: - Social Media // 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 // MARK: - URL Helper Functions
nonisolated private func buildWebURL(_ value: String) -> URL? { nonisolated private func buildWebURL(_ value: String) -> URL? {

View File

@ -606,6 +606,9 @@
} }
} }
} }
},
"The default card is used for sharing and widgets." : {
}, },
"This doesn't appear to be a business card QR code." : { "This doesn't appear to be a business card QR code." : {

View File

@ -44,6 +44,9 @@ struct CardEditorView: View {
@State private var coverPhotoData: Data? @State private var coverPhotoData: Data?
@State private var logoData: Data? @State private var logoData: Data?
// Default card
@State private var isDefault = false
// Photo editor state - just one variable! // Photo editor state - just one variable!
@State private var editingImageType: ImageType? @State private var editingImageType: ImageType?
@ -106,6 +109,15 @@ struct CardEditorView: View {
var body: some View { var body: some View {
NavigationStack { NavigationStack {
Form { 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 // Card Style section
Section { Section {
CardStylePicker(selectedTheme: $selectedTheme) CardStylePicker(selectedTheme: $selectedTheme)
@ -651,6 +663,7 @@ private extension CardEditorView {
bio = card.bio bio = card.bio
accreditations = card.accreditations accreditations = card.accreditations
avatarSystemName = card.avatarSystemName avatarSystemName = card.avatarSystemName
isDefault = card.isDefault
// Load contact fields from the array // Load contact fields from the array
contactFields = card.orderedContactFields.compactMap { $0.toAddedContactField() } contactFields = card.orderedContactFields.compactMap { $0.toAddedContactField() }
@ -693,9 +706,19 @@ private extension CardEditorView {
if let existingCard = card { if let existingCard = card {
updateCard(existingCard) updateCard(existingCard)
onSave(existingCard) onSave(existingCard)
// Handle default card toggle
if isDefault {
appState.cardStore.setDefaultCard(existingCard)
}
} else { } else {
let newCard = createCard() let newCard = createCard()
onSave(newCard) onSave(newCard)
// Handle default card toggle for new cards
if isDefault {
appState.cardStore.setDefaultCard(newCard)
}
} }
dismiss() dismiss()
} }

View File

@ -27,11 +27,7 @@ struct CardsHomeView: View {
// Full-screen swipeable cards // Full-screen swipeable cards
TabView(selection: $cardStore.selectedCardID) { TabView(selection: $cardStore.selectedCardID) {
ForEach(cardStore.cards) { card in ForEach(cardStore.cards) { card in
CardPageView( CardPageView(card: card)
card: card,
isDefault: card.isDefault,
onSetDefault: { cardStore.setDefaultCard(card) }
)
.tag(Optional(card.id)) .tag(Optional(card.id))
} }
} }
@ -97,23 +93,11 @@ struct CardsHomeView: View {
private struct CardPageView: View { private struct CardPageView: View {
let card: BusinessCard let card: BusinessCard
let isDefault: Bool
let onSetDefault: () -> Void
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(spacing: Design.Spacing.large) { VStack(spacing: Design.Spacing.large) {
BusinessCardView(card: card) 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(.horizontal, Design.Spacing.large)
.padding(.vertical, Design.Spacing.xLarge) .padding(.vertical, Design.Spacing.xLarge)