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: 
I am sure I am doing something intensely stupid and would be grateful for help from the community on this.
