Problems with keyboard responsiveness in edit sheet

1 week ago 10
ARTICLE AD BOX

I am building an app in Swift UI and Swift 6 for iOS 26. I am using SwiftData to store content. I have an edit sheet which I am struggling with.

When the sheet loads, the user cannot tap the TextField to edit immediately; it takes multiple taps for the keyboard to appear, and because of that, the text in the field is highlighted for cut, copy, and paste. I have been at this for days, even using AI to see if it can help, and I am no further forward.

My EditPolicyView.swift code:

// // EditPolicyView.swift // Policy Pal // // Created by Justin Erswell on 09/01/2026. // import SwiftUI import SwiftData import PhotosUI // Lightweight attachment summary - no binary data, just metadata for display struct AttachmentSummary: Identifiable, Sendable { let id: UUID let filename: String let mimeType: String let isExisting: Bool // true = already saved in SwiftData, false = newly added var isPDF: Bool { mimeType == "application/pdf" } // Init for existing attachments (extracted values, not the model itself) init(id: UUID, filename: String, mimeType: String, isExisting: Bool) { self.id = id self.filename = filename self.mimeType = mimeType self.isExisting = isExisting } // Convenience init for new attachments init(id: UUID = UUID(), filename: String, mimeType: String) { self.id = id self.filename = filename self.mimeType = mimeType self.isExisting = false } } // Simple value struct to pass data without SwiftData observation // NOTE: Attachments are NOT copied here to avoid blocking main thread with large binary data struct EditPolicyData: Identifiable { let id: PersistentIdentifier var name: String var category: PolicyCategory var provider: String var policyNumber: String var cost: Decimal var costFrequency: CostFrequency var renewalDate: Date var notes: String var reminderThirtyDays: Bool var reminderFourteenDays: Bool var reminderThreeDays: Bool var reminderRenewalDay: Bool init(from policy: PolicyItem) { let start = CFAbsoluteTimeGetCurrent() self.id = policy.persistentModelID print("⏱️ EditPolicyData: persistentModelID took \(CFAbsoluteTimeGetCurrent() - start)s") let t1 = CFAbsoluteTimeGetCurrent() self.name = policy.name self.category = policy.category self.provider = policy.provider self.policyNumber = policy.policyNumber self.cost = policy.cost self.costFrequency = policy.costFrequency self.renewalDate = policy.renewalDate self.notes = policy.notes print("⏱️ EditPolicyData: basic props took \(CFAbsoluteTimeGetCurrent() - t1)s") let t2 = CFAbsoluteTimeGetCurrent() let schedule = policy.reminderSchedule self.reminderThirtyDays = schedule.thirtyDays self.reminderFourteenDays = schedule.fourteenDays self.reminderThreeDays = schedule.threeDays self.reminderRenewalDay = schedule.renewalDay print("⏱️ EditPolicyData: reminderSchedule took \(CFAbsoluteTimeGetCurrent() - t2)s") print("⏱️ EditPolicyData: TOTAL took \(CFAbsoluteTimeGetCurrent() - start)s") } } // Wrapper view that passes data to the actual form struct EditPolicyView: View { let data: EditPolicyData var body: some View { EditPolicyFormView( policyID: data.id, initialName: data.name, initialCategory: data.category, initialProvider: data.provider, initialPolicyNumber: data.policyNumber, initialCost: data.cost, initialCostFrequency: data.costFrequency, initialRenewalDate: data.renewalDate, initialNotes: data.notes, initialReminderThirtyDays: data.reminderThirtyDays, initialReminderFourteenDays: data.reminderFourteenDays, initialReminderThreeDays: data.reminderThreeDays, initialReminderRenewalDay: data.reminderRenewalDay ) } // Convenience init init(data: EditPolicyData) { self.data = data } init(policy: PolicyItem) { self.data = EditPolicyData(from: policy) } } // Actual form view with inline @State initialization (like AddPolicyView) struct EditPolicyFormView: View { @Environment(\.dismiss) private var dismiss @Environment(\.modelContext) private var modelContext @EnvironmentObject private var appSettings: AppSettings // Store the policy ID for saving let policyID: PersistentIdentifier // Initial values passed in let initialName: String let initialCategory: PolicyCategory let initialProvider: String let initialPolicyNumber: String let initialCost: Decimal let initialCostFrequency: CostFrequency let initialRenewalDate: Date let initialNotes: String let initialReminderThirtyDays: Bool let initialReminderFourteenDays: Bool let initialReminderThreeDays: Bool let initialReminderRenewalDay: Bool // Form state - using inline initialization like AddPolicyView @State private var name = "" @State private var category: PolicyCategory = .insurance @State private var provider = "" @State private var policyNumber = "" @State private var cost: Decimal = 0 @State private var costString = "" @State private var costFrequency: CostFrequency = .yearly @State private var renewalDate = Date() @State private var notes = "" // Reminder schedule @State private var reminderThirtyDays = true @State private var reminderFourteenDays = true @State private var reminderThreeDays = true @State private var reminderRenewalDay = true // Track if we've loaded initial values @State private var hasLoadedInitialValues = false // Attachments - use lightweight summaries for display, track changes separately @State private var attachmentSummaries: [AttachmentSummary] = [] @State private var newAttachments: [Attachment] = [] // Newly added attachments (with data) @State private var deletedAttachmentIDs: Set<UUID> = [] // IDs of existing attachments to delete @State private var attachmentsLoaded = false @State private var selectedPhotoItems: [PhotosPickerItem] = [] @State private var showingDocumentScanner = false @State private var showingFilePicker = false @State private var showingValidationError = false @State private var validationErrorMessage = "" // MARK: - Subscription-specific Labels private var isSubscription: Bool { category == .subscription } private var nameFieldLabel: String { isSubscription ? "Subscription Name" : "Name" } private var providerFieldLabel: String { isSubscription ? "Service" : "Provider" } private var referenceFieldLabel: String { isSubscription ? "Account ID (optional)" : "Reference Number" } private var dateFieldLabel: String { isSubscription ? "Next Billing Date" : "Renewal Date" } private var basicInfoSectionHeader: String { isSubscription ? "Subscription Details" : "Basic Information" } private var dateSectionHeader: String { isSubscription ? "Billing" : "Renewal" } private var reminderFooterText: String { isSubscription ? "You'll receive notifications at 9:00 AM before your billing date." : "You'll receive notifications at 9:00 AM on these days." } var body: some View { // Match AddPolicyView structure exactly NavigationStack { Form { // Basic Info Section - minimal test Section { TextField(nameFieldLabel, text: $name) } header: { Text(basicInfoSectionHeader) } } .navigationTitle("Edit Record") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button("Save") { saveChanges() } .disabled(name.isEmpty) } } .alert("Validation Error", isPresented: $showingValidationError) { Button("OK") { } } message: { Text(validationErrorMessage) } .onAppear { // Load initial values only once if !hasLoadedInitialValues { name = initialName category = initialCategory provider = initialProvider policyNumber = initialPolicyNumber cost = initialCost costString = "\(initialCost)" costFrequency = initialCostFrequency renewalDate = initialRenewalDate notes = initialNotes reminderThirtyDays = initialReminderThirtyDays reminderFourteenDays = initialReminderFourteenDays reminderThreeDays = initialReminderThreeDays reminderRenewalDay = initialReminderRenewalDay hasLoadedInitialValues = true } } } /* TEMPORARILY DISABLED - restore after keyboard test .sheet(isPresented: $showingDocumentScanner) { DocumentScannerView { images in processScannedImages(images) } } .sheet(isPresented: $showingFilePicker) { DocumentPickerView { urls in processSelectedFiles(urls) } } .onChange(of: selectedPhotoItems) { _, newItems in processSelectedPhotos(newItems) } .task { // Load attachments in background to avoid blocking UI await loadAttachments() } */ } // Load attachment METADATA only (not binary data) to avoid blocking main thread private func loadAttachments() async { guard !attachmentsLoaded else { return } let start = CFAbsoluteTimeGetCurrent() print("⏱️ loadAttachments: starting...") // Use a background context to avoid blocking main thread let container = modelContext.container let policyIDCopy = policyID // Fetch raw metadata as tuples (Sendable) from background let metadata: [(UUID, String, String)] = await Task.detached { let bgStart = CFAbsoluteTimeGetCurrent() let backgroundContext = ModelContext(container) guard let policy = backgroundContext.model(for: policyIDCopy) as? PolicyItem else { return [] } // Only access metadata properties, NOT the data property let result = policy.safeAttachments.map { ($0.id, $0.filename, $0.mimeType) } print("⏱️ loadAttachments background task took \(CFAbsoluteTimeGetCurrent() - bgStart)s") return result }.value // Create summaries on main actor attachmentSummaries = metadata.map { AttachmentSummary(id: $0.0, filename: $0.1, mimeType: $0.2, isExisting: true) } attachmentsLoaded = true print("⏱️ loadAttachments: TOTAL took \(CFAbsoluteTimeGetCurrent() - start)s") } // MARK: - Save Changes private func saveChanges() { guard !name.trimmingCharacters(in: .whitespaces).isEmpty else { validationErrorMessage = "Please enter a name." showingValidationError = true return } // Fetch the policy by ID guard let policy = modelContext.model(for: policyID) as? PolicyItem else { validationErrorMessage = "Could not find record to update." showingValidationError = true return } policy.name = name.trimmingCharacters(in: .whitespaces) policy.category = category policy.provider = provider.trimmingCharacters(in: .whitespaces) policy.policyNumber = policyNumber.trimmingCharacters(in: .whitespaces) policy.cost = cost policy.costFrequency = costFrequency policy.renewalDate = renewalDate policy.notes = notes.trimmingCharacters(in: .whitespaces) policy.updatedAt = Date() policy.reminderSchedule = ReminderSchedule( thirtyDays: reminderThirtyDays, fourteenDays: reminderFourteenDays, threeDays: reminderThreeDays, renewalDay: reminderRenewalDay ) // Only modify attachments that changed (not rewriting everything) // 1. Remove deleted attachments if !deletedAttachmentIDs.isEmpty { policy.safeAttachments.removeAll { deletedAttachmentIDs.contains($0.id) } } // 2. Add new attachments for attachment in newAttachments { policy.safeAttachments.append(attachment) } // Reschedule notifications Task { await NotificationManager.shared.scheduleNotifications(for: policy) } dismiss() } // MARK: - Attachment Handling private func removeAttachment(_ summary: AttachmentSummary) { attachmentSummaries.removeAll { $0.id == summary.id } if summary.isExisting { // Mark existing attachment for deletion on save deletedAttachmentIDs.insert(summary.id) } else { // Remove newly added attachment newAttachments.removeAll { $0.id == summary.id } } } private func processScannedImages(_ images: [UIImage]) { for (index, image) in images.enumerated() { if let data = image.jpegData(compressionQuality: 0.8) { let id = UUID() let filename = "scan_\(attachmentSummaries.count + index + 1).jpg" let mimeType = "image/jpeg" // Add to newAttachments (with data) for saving let attachment = Attachment(filename: filename, data: data, mimeType: mimeType) attachment.id = id newAttachments.append(attachment) // Add summary for display attachmentSummaries.append(AttachmentSummary(id: id, filename: filename, mimeType: mimeType)) } } } private func processSelectedPhotos(_ items: [PhotosPickerItem]) { for item in items { Task { if let data = try? await item.loadTransferable(type: Data.self) { await MainActor.run { let id = UUID() let filename = "photo_\(attachmentSummaries.count + 1).jpg" let mimeType = "image/jpeg" // Add to newAttachments (with data) for saving let attachment = Attachment(filename: filename, data: data, mimeType: mimeType) attachment.id = id newAttachments.append(attachment) // Add summary for display attachmentSummaries.append(AttachmentSummary(id: id, filename: filename, mimeType: mimeType)) } } } } selectedPhotoItems = [] } private func processSelectedFiles(_ urls: [URL]) { for url in urls { guard url.startAccessingSecurityScopedResource() else { continue } defer { url.stopAccessingSecurityScopedResource() } if let data = try? Data(contentsOf: url) { let id = UUID() let filename = url.lastPathComponent let mimeType = url.pathExtension.lowercased() == "pdf" ? "application/pdf" : "image/jpeg" // Add to newAttachments (with data) for saving let attachment = Attachment(filename: filename, data: data, mimeType: mimeType) attachment.id = id newAttachments.append(attachment) // Add summary for display attachmentSummaries.append(AttachmentSummary(id: id, filename: filename, mimeType: mimeType)) } } } } #Preview { EditPolicyView(policy: PolicyItem( name: "Test Policy", category: .insurance, provider: "Test Provider", renewalDate: Date() )) .modelContainer(for: PolicyItem.self, inMemory: true) .environmentObject(AppSettings.shared) }

Also A screenshot of the view running on an iPhone 17 Pro Max: EditPolicyView Test on iPhone. Shows TextField entry higlight

I am sure I am doing something intensely stupid and would be grateful for help from the community on this.

Read Entire Article