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
}
/// 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 {

View File

@ -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? {

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." : {

View File

@ -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()
}

View File

@ -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)