Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
540bd095dd
commit
3e9436791a
@ -9,9 +9,10 @@ struct BusinessCardApp: App {
|
||||
|
||||
init() {
|
||||
let schema = Schema([BusinessCard.self, Contact.self, ContactField.self])
|
||||
let cloudKitDatabase: ModelConfiguration.CloudKitDatabase = .private(
|
||||
AppIdentifiers.cloudKitContainerIdentifier
|
||||
)
|
||||
let cloudKitDatabase: ModelConfiguration.CloudKitDatabase =
|
||||
AppIdentifiers.isCloudKitSyncEnabled
|
||||
? .private(AppIdentifiers.cloudKitContainerIdentifier)
|
||||
: .none
|
||||
|
||||
// Register app theme for Bedrock semantic text/surface colors.
|
||||
Theme.register(
|
||||
@ -22,39 +23,55 @@ struct BusinessCardApp: App {
|
||||
)
|
||||
Theme.register(border: AppBorder.self)
|
||||
|
||||
// Primary strategy: App Group-backed persistent store with CloudKit sync.
|
||||
// Primary strategy: App Group-backed persistent store.
|
||||
var container: ModelContainer?
|
||||
|
||||
if let appGroupURL = FileManager.default.containerURL(
|
||||
forSecurityApplicationGroupIdentifier: AppIdentifiers.appGroupIdentifier
|
||||
) {
|
||||
let storeURL = appGroupURL.appending(path: "BusinessCard.store")
|
||||
func makeContainer(storeURL: URL, cloudKitDatabase: ModelConfiguration.CloudKitDatabase) -> ModelContainer? {
|
||||
let config = ModelConfiguration(
|
||||
schema: schema,
|
||||
url: storeURL,
|
||||
cloudKitDatabase: cloudKitDatabase
|
||||
)
|
||||
return try? ModelContainer(for: schema, configurations: [config])
|
||||
}
|
||||
|
||||
do {
|
||||
container = try ModelContainer(for: schema, configurations: [config])
|
||||
} catch {
|
||||
Design.debugLog("Failed to create container with App Group: \(error)")
|
||||
if let appGroupURL = FileManager.default.containerURL(
|
||||
forSecurityApplicationGroupIdentifier: AppIdentifiers.appGroupIdentifier
|
||||
) {
|
||||
let storeURL = appGroupURL.appending(path: "BusinessCard.store")
|
||||
container = makeContainer(storeURL: storeURL, cloudKitDatabase: cloudKitDatabase)
|
||||
if container == nil {
|
||||
Design.debugLog("Failed to create container in App Group.")
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Default location (Application Support)
|
||||
if container == nil {
|
||||
let storeURL = URL.applicationSupportDirectory.appending(path: "BusinessCard.store")
|
||||
let config = ModelConfiguration(
|
||||
schema: schema,
|
||||
url: storeURL,
|
||||
cloudKitDatabase: cloudKitDatabase
|
||||
)
|
||||
container = makeContainer(storeURL: storeURL, cloudKitDatabase: cloudKitDatabase)
|
||||
if container == nil {
|
||||
Design.debugLog("Failed to create container in Application Support.")
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
container = try ModelContainer(for: schema, configurations: [config])
|
||||
} catch {
|
||||
Design.debugLog("Failed to create container in Application Support: \(error)")
|
||||
// If CloudKit mode fails, force local-on-disk fallback before in-memory.
|
||||
if container == nil && AppIdentifiers.isCloudKitSyncEnabled {
|
||||
if let appGroupURL = FileManager.default.containerURL(
|
||||
forSecurityApplicationGroupIdentifier: AppIdentifiers.appGroupIdentifier
|
||||
) {
|
||||
let storeURL = appGroupURL.appending(path: "BusinessCard.store")
|
||||
container = makeContainer(storeURL: storeURL, cloudKitDatabase: .none)
|
||||
if container == nil {
|
||||
Design.debugLog("Failed to create local-only container in App Group.")
|
||||
}
|
||||
}
|
||||
|
||||
if container == nil {
|
||||
let storeURL = URL.applicationSupportDirectory.appending(path: "BusinessCard.store")
|
||||
container = makeContainer(storeURL: storeURL, cloudKitDatabase: .none)
|
||||
if container == nil {
|
||||
Design.debugLog("Failed to create local-only container in Application Support.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -25,6 +25,13 @@ enum AppIdentifiers {
|
||||
?? "iCloud.com.mbrucedogs.BusinessCard"
|
||||
}()
|
||||
|
||||
/// Whether CloudKit-backed SwiftData sync should be enabled.
|
||||
/// Defaults to false so local persistence works even when CloudKit model constraints are unmet.
|
||||
static let isCloudKitSyncEnabled: Bool = {
|
||||
let rawValue = (Bundle.main.object(forInfoDictionaryKey: "CloudKitSyncEnabled") as? String) ?? "NO"
|
||||
return NSString(string: rawValue).boolValue
|
||||
}()
|
||||
|
||||
/// App Clip domain for sharing URLs.
|
||||
static let appClipDomain: String = {
|
||||
Bundle.main.object(forInfoDictionaryKey: "AppClipDomain") as? String
|
||||
|
||||
@ -26,6 +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
|
||||
|
||||
// =============================================================================
|
||||
// APP CLIP CONFIGURATION
|
||||
|
||||
@ -10,6 +10,8 @@
|
||||
<string>$(APP_GROUP_IDENTIFIER)</string>
|
||||
<key>CloudKitContainerIdentifier</key>
|
||||
<string>$(CLOUDKIT_CONTAINER_IDENTIFIER)</string>
|
||||
<key>CloudKitSyncEnabled</key>
|
||||
<string>$(CLOUDKIT_SYNC_ENABLED)</string>
|
||||
<key>AppClipDomain</key>
|
||||
<string>$(APPCLIP_DOMAIN)</string>
|
||||
<key>UILaunchScreen</key>
|
||||
|
||||
@ -151,7 +151,8 @@ extension ContactFieldType {
|
||||
urlBuilder: { value in
|
||||
let digits = value.filter { $0.isNumber || $0 == "+" }
|
||||
return URL(string: "tel:\(digits)")
|
||||
}
|
||||
},
|
||||
displayValueFormatter: formatPhoneForDisplay
|
||||
)
|
||||
|
||||
static let email = ContactFieldType(
|
||||
@ -676,6 +677,12 @@ nonisolated private func formatAddressForDisplay(_ value: String) -> String {
|
||||
return trimmed
|
||||
}
|
||||
|
||||
/// Formats a phone number for display with lightweight US-first grouping.
|
||||
/// Falls back to a spaced international style for non-US lengths.
|
||||
nonisolated private func formatPhoneForDisplay(_ value: String) -> String {
|
||||
PhoneNumberText.formatted(value)
|
||||
}
|
||||
|
||||
// MARK: - URL Helper Functions
|
||||
|
||||
nonisolated private func buildWebURL(_ value: String) -> URL? {
|
||||
@ -706,3 +713,134 @@ nonisolated private func buildSocialURL(_ value: String, webBase: String) -> URL
|
||||
let username = trimmed.hasPrefix("@") ? String(trimmed.dropFirst()) : trimmed
|
||||
return URL(string: "https://\(webBase)/\(username)")
|
||||
}
|
||||
|
||||
// MARK: - Phone Text Utilities
|
||||
|
||||
enum PhoneNumberText {
|
||||
nonisolated static func normalizedForStorage(_ value: String) -> String {
|
||||
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let hasLeadingPlus = trimmed.hasPrefix("+")
|
||||
let digits = trimmed.filter(\.isNumber)
|
||||
|
||||
guard !digits.isEmpty else { return "" }
|
||||
if hasLeadingPlus {
|
||||
return "+\(digits)"
|
||||
}
|
||||
return digits
|
||||
}
|
||||
|
||||
nonisolated static func isValid(_ value: String) -> Bool {
|
||||
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return false }
|
||||
|
||||
let plusCount = trimmed.filter { $0 == "+" }.count
|
||||
if plusCount > 1 { return false }
|
||||
if plusCount == 1 && !trimmed.hasPrefix("+") { return false }
|
||||
|
||||
let digits = trimmed.filter(\.isNumber)
|
||||
return (7...15).contains(digits.count)
|
||||
}
|
||||
|
||||
nonisolated static func formatted(_ raw: String) -> String {
|
||||
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return "" }
|
||||
|
||||
let hasLeadingPlus = trimmed.hasPrefix("+")
|
||||
let digits = trimmed.filter(\.isNumber)
|
||||
guard !digits.isEmpty else { return hasLeadingPlus ? "+" : "" }
|
||||
|
||||
if hasLeadingPlus {
|
||||
if digits.count == 11, digits.first == "1" {
|
||||
return "+1 \(formatUSDigits(String(digits.dropFirst())))"
|
||||
}
|
||||
return "+" + formatInternationalDigits(digits)
|
||||
}
|
||||
|
||||
if digits.count == 11, digits.first == "1" {
|
||||
return "1 \(formatUSDigits(String(digits.dropFirst())))"
|
||||
}
|
||||
if digits.count <= 10 {
|
||||
return formatUSDigits(digits)
|
||||
}
|
||||
return formatInternationalDigits(digits)
|
||||
}
|
||||
|
||||
nonisolated private static func formatUSDigits(_ digits: String) -> String {
|
||||
if digits.isEmpty { return "" }
|
||||
|
||||
if digits.count <= 3 {
|
||||
return digits
|
||||
}
|
||||
|
||||
let area = String(digits.prefix(3))
|
||||
let remaining = String(digits.dropFirst(3))
|
||||
|
||||
if remaining.count <= 3 {
|
||||
return "(\(area)) \(remaining)"
|
||||
}
|
||||
|
||||
let prefix = String(remaining.prefix(3))
|
||||
let line = String(remaining.dropFirst(3).prefix(4))
|
||||
return "(\(area)) \(prefix)-\(line)"
|
||||
}
|
||||
|
||||
nonisolated private static func formatInternationalDigits(_ digits: String) -> String {
|
||||
guard !digits.isEmpty else { return "" }
|
||||
|
||||
if digits.count <= 3 {
|
||||
return digits
|
||||
}
|
||||
|
||||
var groups: [String] = []
|
||||
var index = digits.startIndex
|
||||
|
||||
while index < digits.endIndex {
|
||||
let next = digits.index(index, offsetBy: 3, limitedBy: digits.endIndex) ?? digits.endIndex
|
||||
groups.append(String(digits[index..<next]))
|
||||
index = next
|
||||
}
|
||||
|
||||
return groups.joined(separator: " ")
|
||||
}
|
||||
}
|
||||
|
||||
enum EmailText {
|
||||
nonisolated static func normalizedForStorage(_ value: String) -> String {
|
||||
value
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.lowercased()
|
||||
}
|
||||
|
||||
nonisolated static func isValid(_ value: String) -> Bool {
|
||||
let trimmed = normalizedForStorage(value)
|
||||
guard !trimmed.isEmpty, !trimmed.contains(" ") else { return false }
|
||||
|
||||
let parts = trimmed.split(separator: "@", omittingEmptySubsequences: false)
|
||||
guard parts.count == 2 else { return false }
|
||||
|
||||
let local = String(parts[0])
|
||||
let domain = String(parts[1])
|
||||
|
||||
guard !local.isEmpty, !domain.isEmpty else { return false }
|
||||
guard domain.contains("."), !domain.hasPrefix("."), !domain.hasSuffix(".") else { return false }
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
enum WebLinkText {
|
||||
nonisolated static func normalizedForStorage(_ value: String) -> String {
|
||||
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return "" }
|
||||
if let url = buildWebURL(trimmed) {
|
||||
return url.absoluteString
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
nonisolated static func isValid(_ value: String) -> Bool {
|
||||
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty, !trimmed.contains(" ") else { return false }
|
||||
guard let url = buildWebURL(trimmed) else { return false }
|
||||
return url.host != nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,7 +149,8 @@ final class ContactsStore: ContactTracking {
|
||||
do {
|
||||
try modelContext.save()
|
||||
} catch {
|
||||
// Handle error silently for now
|
||||
modelContext.rollback()
|
||||
print("ContactsStore save failed: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,8 +36,30 @@ struct AddContactSheet: View {
|
||||
@State private var notes = ""
|
||||
|
||||
private var canSave: Bool {
|
||||
!firstName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ||
|
||||
let hasName = !firstName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ||
|
||||
!lastName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
return hasName && !hasInvalidPhoneEntry && !hasInvalidEmailEntry && !hasInvalidLinkEntry
|
||||
}
|
||||
|
||||
private var hasInvalidPhoneEntry: Bool {
|
||||
phoneEntries.contains { entry in
|
||||
let trimmed = entry.value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return !trimmed.isEmpty && !PhoneNumberText.isValid(trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
private var hasInvalidEmailEntry: Bool {
|
||||
emailEntries.contains { entry in
|
||||
let trimmed = entry.value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return !trimmed.isEmpty && !EmailText.isValid(trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
private var hasInvalidLinkEntry: Bool {
|
||||
linkEntries.contains { entry in
|
||||
let trimmed = entry.value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return !trimmed.isEmpty && !WebLinkText.isValid(trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
private var fullName: String {
|
||||
@ -83,7 +105,11 @@ struct AddContactSheet: View {
|
||||
entry: $entry,
|
||||
valuePlaceholder: "+1 (555) 123-4567",
|
||||
labelSuggestions: ["Cell", "Work", "Home", "Main"],
|
||||
keyboardType: .phonePad
|
||||
keyboardType: .phonePad,
|
||||
autocapitalization: .never,
|
||||
formatValue: PhoneNumberText.formatted,
|
||||
isValueValid: PhoneNumberText.isValid,
|
||||
validationMessage: String.localized("Enter a valid phone number")
|
||||
)
|
||||
}
|
||||
.onDelete { indexSet in
|
||||
@ -107,7 +133,9 @@ struct AddContactSheet: View {
|
||||
valuePlaceholder: "email@example.com",
|
||||
labelSuggestions: ["Work", "Personal", "Other"],
|
||||
keyboardType: .emailAddress,
|
||||
autocapitalization: .never
|
||||
autocapitalization: .never,
|
||||
isValueValid: EmailText.isValid,
|
||||
validationMessage: String.localized("Enter a valid email address")
|
||||
)
|
||||
}
|
||||
.onDelete { indexSet in
|
||||
@ -131,7 +159,9 @@ struct AddContactSheet: View {
|
||||
valuePlaceholder: "https://example.com",
|
||||
labelSuggestions: ["Website", "Portfolio", "LinkedIn", "Other"],
|
||||
keyboardType: .URL,
|
||||
autocapitalization: .never
|
||||
autocapitalization: .never,
|
||||
isValueValid: WebLinkText.isValid,
|
||||
validationMessage: String.localized("Enter a valid web link")
|
||||
)
|
||||
}
|
||||
.onDelete { indexSet in
|
||||
@ -231,22 +261,49 @@ struct AddContactSheet: View {
|
||||
var orderIndex = 0
|
||||
|
||||
// Add phone entries
|
||||
for entry in phoneEntries where !entry.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
let field = ContactField(typeId: "phone", value: entry.value.trimmingCharacters(in: .whitespacesAndNewlines), title: entry.label, orderIndex: orderIndex)
|
||||
for entry in phoneEntries {
|
||||
let trimmed = entry.value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { continue }
|
||||
guard PhoneNumberText.isValid(trimmed) else { continue }
|
||||
|
||||
let field = ContactField(
|
||||
typeId: "phone",
|
||||
value: PhoneNumberText.normalizedForStorage(trimmed),
|
||||
title: entry.label,
|
||||
orderIndex: orderIndex
|
||||
)
|
||||
contactFields.append(field)
|
||||
orderIndex += 1
|
||||
}
|
||||
|
||||
// Add email entries
|
||||
for entry in emailEntries where !entry.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
let field = ContactField(typeId: "email", value: entry.value.trimmingCharacters(in: .whitespacesAndNewlines), title: entry.label, orderIndex: orderIndex)
|
||||
for entry in emailEntries {
|
||||
let trimmed = entry.value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { continue }
|
||||
guard EmailText.isValid(trimmed) else { continue }
|
||||
|
||||
let field = ContactField(
|
||||
typeId: "email",
|
||||
value: EmailText.normalizedForStorage(trimmed),
|
||||
title: entry.label,
|
||||
orderIndex: orderIndex
|
||||
)
|
||||
contactFields.append(field)
|
||||
orderIndex += 1
|
||||
}
|
||||
|
||||
// Add link entries
|
||||
for entry in linkEntries where !entry.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
let field = ContactField(typeId: "customLink", value: entry.value.trimmingCharacters(in: .whitespacesAndNewlines), title: entry.label, orderIndex: orderIndex)
|
||||
for entry in linkEntries {
|
||||
let trimmed = entry.value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { continue }
|
||||
guard WebLinkText.isValid(trimmed) else { continue }
|
||||
|
||||
let field = ContactField(
|
||||
typeId: "customLink",
|
||||
value: WebLinkText.normalizedForStorage(trimmed),
|
||||
title: entry.label,
|
||||
orderIndex: orderIndex
|
||||
)
|
||||
contactFields.append(field)
|
||||
orderIndex += 1
|
||||
}
|
||||
@ -327,8 +384,22 @@ private struct LabeledFieldRow: View {
|
||||
let labelSuggestions: [String]
|
||||
var keyboardType: UIKeyboardType = .default
|
||||
var autocapitalization: TextInputAutocapitalization = .sentences
|
||||
var formatValue: ((String) -> String)?
|
||||
var isValueValid: ((String) -> Bool)?
|
||||
var validationMessage: String?
|
||||
|
||||
private var trimmedValue: String {
|
||||
entry.value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
private var showsValidationError: Bool {
|
||||
guard let isValueValid else { return false }
|
||||
guard !trimmedValue.isEmpty else { return false }
|
||||
return !isValueValid(trimmedValue)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
|
||||
HStack(spacing: Design.Spacing.medium) {
|
||||
// Label picker
|
||||
Menu {
|
||||
@ -352,6 +423,21 @@ private struct LabeledFieldRow: View {
|
||||
TextField(valuePlaceholder, text: $entry.value)
|
||||
.keyboardType(keyboardType)
|
||||
.textInputAutocapitalization(autocapitalization)
|
||||
.onChange(of: entry.value) { _, newValue in
|
||||
guard let formatValue else { return }
|
||||
let formatted = formatValue(newValue)
|
||||
if formatted != newValue {
|
||||
entry.value = formatted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if showsValidationError, let validationMessage {
|
||||
Text(validationMessage)
|
||||
.typography(.caption)
|
||||
.foregroundStyle(Color.Accent.red)
|
||||
.padding(.leading, 80 + Design.Spacing.medium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,10 +49,31 @@ struct ContactFieldEditorSheet: View {
|
||||
fieldType.id == "address"
|
||||
}
|
||||
|
||||
private var isPhoneField: Bool {
|
||||
fieldType.id == "phone"
|
||||
}
|
||||
|
||||
private var isEmailField: Bool {
|
||||
fieldType.id == "email"
|
||||
}
|
||||
|
||||
private var isStrictURLField: Bool {
|
||||
fieldType.id == "website" || fieldType.id == "customLink" || fieldType.id == "calendly"
|
||||
}
|
||||
|
||||
private var isValid: Bool {
|
||||
if isAddressField {
|
||||
return postalAddress.hasValue
|
||||
}
|
||||
if isPhoneField {
|
||||
return PhoneNumberText.isValid(value)
|
||||
}
|
||||
if isEmailField {
|
||||
return EmailText.isValid(value)
|
||||
}
|
||||
if isStrictURLField {
|
||||
return WebLinkText.isValid(value)
|
||||
}
|
||||
return !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
}
|
||||
|
||||
@ -83,6 +104,33 @@ struct ContactFieldEditorSheet: View {
|
||||
.keyboardType(fieldType.keyboardType)
|
||||
.textInputAutocapitalization(fieldType.autocapitalization)
|
||||
.textContentType(textContentType)
|
||||
.onChange(of: value) { _, newValue in
|
||||
guard isPhoneField else { return }
|
||||
let formatted = PhoneNumberText.formatted(newValue)
|
||||
if formatted != newValue {
|
||||
value = formatted
|
||||
}
|
||||
}
|
||||
|
||||
if isPhoneField,
|
||||
!value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
||||
!PhoneNumberText.isValid(value) {
|
||||
Text(String.localized("Enter a valid phone number"))
|
||||
.typography(.caption)
|
||||
.foregroundStyle(Color.Accent.red)
|
||||
} else if isEmailField,
|
||||
!value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
||||
!EmailText.isValid(value) {
|
||||
Text(String.localized("Enter a valid email address"))
|
||||
.typography(.caption)
|
||||
.foregroundStyle(Color.Accent.red)
|
||||
} else if isStrictURLField,
|
||||
!value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
||||
!WebLinkText.isValid(value) {
|
||||
Text(String.localized("Enter a valid web link"))
|
||||
.typography(.caption)
|
||||
.foregroundStyle(Color.Accent.red)
|
||||
}
|
||||
|
||||
Divider()
|
||||
}
|
||||
@ -157,7 +205,14 @@ struct ContactFieldEditorSheet: View {
|
||||
// Save postal address as JSON
|
||||
onSave(postalAddress.encode(), title)
|
||||
} else {
|
||||
onSave(value.trimmingCharacters(in: .whitespacesAndNewlines), title)
|
||||
let valueToSave = isPhoneField
|
||||
? PhoneNumberText.normalizedForStorage(value)
|
||||
: isEmailField
|
||||
? EmailText.normalizedForStorage(value)
|
||||
: isStrictURLField
|
||||
? WebLinkText.normalizedForStorage(value)
|
||||
: value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
onSave(valueToSave, title)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user