HomeiOS DevelopmentIssues with keyboard responsiveness in edit sheet

Issues with keyboard responsiveness in edit sheet


I’m constructing an app in Swift UI and Swift 6 for iOS 26. I’m utilizing SwiftData to retailer content material. I’ve an edit sheet which I’m combating.

When the sheet hundreds, the person can’t faucet the TextField to edit instantly; it takes a number of faucets for the keyboard to look, and due to that, the textual content within the discipline is highlighted for lower, copy, and paste. I’ve been at this for days, even utilizing AI to see if it will possibly assist, and I’m no additional ahead.

My EditPolicyView.swift code:

//
//  EditPolicyView.swift
//  Coverage Pal
//
//  Created by Justin Erswell on 09/01/2026.
//

import SwiftUI
import SwiftData
import PhotosUI

// Light-weight attachment abstract - no binary information, simply metadata for show
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 == "utility/pdf" }

    // Init for present attachments (extracted values, not the mannequin itself)
    init(id: UUID, filename: String, mimeType: String, isExisting: Bool) {
        self.id = id
        self.filename = filename
        self.mimeType = mimeType
        self.isExisting = isExisting
    }

    // Comfort init for brand spanking new attachments
    init(id: UUID = UUID(), filename: String, mimeType: String) {
        self.id = id
        self.filename = filename
        self.mimeType = mimeType
        self.isExisting = false
    }
}

// Easy worth struct to move information with out SwiftData statement
// NOTE: Attachments are NOT copied right here to keep away from blocking primary thread with massive binary information
struct EditPolicyData: Identifiable {
    let id: PersistentIdentifier
    var identify: String
    var class: PolicyCategory
    var supplier: String
    var policyNumber: String
    var price: Decimal
    var costFrequency: CostFrequency
    var renewalDate: Date
    var notes: String
    var reminderThirtyDays: Bool
    var reminderFourteenDays: Bool
    var reminderThreeDays: Bool
    var reminderRenewalDay: Bool

    init(from coverage: PolicyItem) {
        let begin = CFAbsoluteTimeGetCurrent()
        self.id = coverage.persistentModelID
        print("⏱️ EditPolicyData: persistentModelID took (CFAbsoluteTimeGetCurrent() - begin)s")

        let t1 = CFAbsoluteTimeGetCurrent()
        self.identify = coverage.identify
        self.class = coverage.class
        self.supplier = coverage.supplier
        self.policyNumber = coverage.policyNumber
        self.price = coverage.price
        self.costFrequency = coverage.costFrequency
        self.renewalDate = coverage.renewalDate
        self.notes = coverage.notes
        print("⏱️ EditPolicyData: primary props took (CFAbsoluteTimeGetCurrent() - t1)s")

        let t2 = CFAbsoluteTimeGetCurrent()
        let schedule = coverage.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() - begin)s")
    }
}

// Wrapper view that passes information to the precise kind
struct EditPolicyView: View {
    let information: EditPolicyData

    var physique: some View {
        EditPolicyFormView(
            policyID: information.id,
            initialName: information.identify,
            initialCategory: information.class,
            initialProvider: information.supplier,
            initialPolicyNumber: information.policyNumber,
            initialCost: information.price,
            initialCostFrequency: information.costFrequency,
            initialRenewalDate: information.renewalDate,
            initialNotes: information.notes,
            initialReminderThirtyDays: information.reminderThirtyDays,
            initialReminderFourteenDays: information.reminderFourteenDays,
            initialReminderThreeDays: information.reminderThreeDays,
            initialReminderRenewalDay: information.reminderRenewalDay
        )
    }

    // Comfort init
    init(information: EditPolicyData) {
        self.information = information
    }

    init(coverage: PolicyItem) {
        self.information = EditPolicyData(from: coverage)
    }
}

// Precise kind view with inline @State initialization (like AddPolicyView)
struct EditPolicyFormView: View {
    @Setting(.dismiss) non-public var dismiss
    @Setting(.modelContext) non-public var modelContext
    @EnvironmentObject non-public var appSettings: AppSettings

    // Retailer the coverage ID for saving
    let policyID: PersistentIdentifier

    // Preliminary values handed 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

    // Kind state - utilizing inline initialization like AddPolicyView
    @State non-public var identify = ""
    @State non-public var class: PolicyCategory = .insurance coverage
    @State non-public var supplier = ""
    @State non-public var policyNumber = ""
    @State non-public var price: Decimal = 0
    @State non-public var costString = ""
    @State non-public var costFrequency: CostFrequency = .yearly
    @State non-public var renewalDate = Date()
    @State non-public var notes = ""

    // Reminder schedule
    @State non-public var reminderThirtyDays = true
    @State non-public var reminderFourteenDays = true
    @State non-public var reminderThreeDays = true
    @State non-public var reminderRenewalDay = true

    // Monitor if we have loaded preliminary values
    @State non-public var hasLoadedInitialValues = false

    // Attachments - use light-weight summaries for show, monitor adjustments individually
    @State non-public var attachmentSummaries: [AttachmentSummary] = []
    @State non-public var newAttachments: [Attachment] = []  // Newly added attachments (with information)
    @State non-public var deletedAttachmentIDs: Set<UUID> = []  // IDs of present attachments to delete
    @State non-public var attachmentsLoaded = false
    @State non-public var selectedPhotoItems: [PhotosPickerItem] = []
    @State non-public var showingDocumentScanner = false
    @State non-public var showingFilePicker = false

    @State non-public var showingValidationError = false
    @State non-public var validationErrorMessage = ""

    // MARK: - Subscription-specific Labels
    non-public var isSubscription: Bool {
        class == .subscription
    }

    non-public var nameFieldLabel: String {
        isSubscription ? "Subscription Identify" : "Identify"
    }

    non-public var providerFieldLabel: String {
        isSubscription ? "Service" : "Supplier"
    }

    non-public var referenceFieldLabel: String {
        isSubscription ? "Account ID (optionally available)" : "Reference Quantity"
    }

    non-public var dateFieldLabel: String {
        isSubscription ? "Subsequent Billing Date" : "Renewal Date"
    }

    non-public var basicInfoSectionHeader: String {
        isSubscription ? "Subscription Particulars" : "Primary Info"
    }

    non-public var dateSectionHeader: String {
        isSubscription ? "Billing" : "Renewal"
    }

    non-public var reminderFooterText: String {
        isSubscription
            ? "You may obtain notifications at 9:00 AM earlier than your billing date."
            : "You may obtain notifications at 9:00 AM on nowadays."
    }

    var physique: some View {
        // Match AddPolicyView construction precisely
        NavigationStack {
            Kind {
                // Primary Information Part - minimal take a look at
                Part {
                    TextField(nameFieldLabel, textual content: $identify)
                } header: {
                    Textual content(basicInfoSectionHeader)
                }
            }
            .navigationTitle("Edit Report")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("Cancel") {
                        dismiss()
                    }
                }
                ToolbarItem(placement: .confirmationAction) {
                    Button("Save") {
                        saveChanges()
                    }
                    .disabled(identify.isEmpty)
                }
            }
            .alert("Validation Error", isPresented: $showingValidationError) {
                Button("OK") { }
            } message: {
                Textual content(validationErrorMessage)
            }
            .onAppear {
                // Load preliminary values solely as soon as
                if !hasLoadedInitialValues {
                    identify = initialName
                    class = initialCategory
                    supplier = initialProvider
                    policyNumber = initialPolicyNumber
                    price = initialCost
                    costString = "(initialCost)"
                    costFrequency = initialCostFrequency
                    renewalDate = initialRenewalDate
                    notes = initialNotes
                    reminderThirtyDays = initialReminderThirtyDays
                    reminderFourteenDays = initialReminderFourteenDays
                    reminderThreeDays = initialReminderThreeDays
                    reminderRenewalDay = initialReminderRenewalDay
                    hasLoadedInitialValues = true
                }
            }
        }
        /* TEMPORARILY DISABLED - restore after keyboard take a look at
        .sheet(isPresented: $showingDocumentScanner) {
            DocumentScannerView { pictures in
                processScannedImages(pictures)
            }
        }
        .sheet(isPresented: $showingFilePicker) {
            DocumentPickerView { urls in
                processSelectedFiles(urls)
            }
        }
        .onChange(of: selectedPhotoItems) { _, newItems in
            processSelectedPhotos(newItems)
        }
        .activity {
            // Load attachments in background to keep away from blocking UI
            await loadAttachments()
        }
        */
    }

    // Load attachment METADATA solely (not binary information) to keep away from blocking primary thread
    non-public func loadAttachments() async {
        guard !attachmentsLoaded else { return }
        let begin = CFAbsoluteTimeGetCurrent()
        print("⏱️ loadAttachments: beginning...")

        // Use a background context to keep away from blocking primary thread
        let container = modelContext.container
        let policyIDCopy = policyID

        // Fetch uncooked metadata as tuples (Sendable) from background
        let metadata: [(UUID, String, String)] = await Job.indifferent {
            let bgStart = CFAbsoluteTimeGetCurrent()
            let backgroundContext = ModelContext(container)
            guard let coverage = backgroundContext.mannequin(for: policyIDCopy) as? PolicyItem else {
                return []
            }
            // Solely entry metadata properties, NOT the information property
            let end result = coverage.safeAttachments.map { ($0.id, $0.filename, $0.mimeType) }
            print("⏱️ loadAttachments background activity took (CFAbsoluteTimeGetCurrent() - bgStart)s")
            return end result
        }.worth

        // Create summaries on primary actor
        attachmentSummaries = metadata.map {
            AttachmentSummary(id: $0.0, filename: $0.1, mimeType: $0.2, isExisting: true)
        }
        attachmentsLoaded = true
        print("⏱️ loadAttachments: TOTAL took (CFAbsoluteTimeGetCurrent() - begin)s")
    }

    // MARK: - Save Modifications
    non-public func saveChanges() {
        guard !identify.trimmingCharacters(in: .whitespaces).isEmpty else {
            validationErrorMessage = "Please enter a reputation."
            showingValidationError = true
            return
        }

        // Fetch the coverage by ID
        guard let coverage = modelContext.mannequin(for: policyID) as? PolicyItem else {
            validationErrorMessage = "Couldn't discover report to replace."
            showingValidationError = true
            return
        }

        coverage.identify = identify.trimmingCharacters(in: .whitespaces)
        coverage.class = class
        coverage.supplier = supplier.trimmingCharacters(in: .whitespaces)
        coverage.policyNumber = policyNumber.trimmingCharacters(in: .whitespaces)
        coverage.price = price
        coverage.costFrequency = costFrequency
        coverage.renewalDate = renewalDate
        coverage.notes = notes.trimmingCharacters(in: .whitespaces)
        coverage.updatedAt = Date()

        coverage.reminderSchedule = ReminderSchedule(
            thirtyDays: reminderThirtyDays,
            fourteenDays: reminderFourteenDays,
            threeDays: reminderThreeDays,
            renewalDay: reminderRenewalDay
        )

        // Solely modify attachments that modified (not rewriting every part)
        // 1. Take away deleted attachments
        if !deletedAttachmentIDs.isEmpty {
            coverage.safeAttachments.removeAll { deletedAttachmentIDs.incorporates($0.id) }
        }

        // 2. Add new attachments
        for attachment in newAttachments {
            coverage.safeAttachments.append(attachment)
        }

        // Reschedule notifications
        Job {
            await NotificationManager.shared.scheduleNotifications(for: coverage)
        }

        dismiss()
    }

    // MARK: - Attachment Dealing with
    non-public func removeAttachment(_ abstract: AttachmentSummary) {
        attachmentSummaries.removeAll { $0.id == abstract.id }
        if abstract.isExisting {
            // Mark present attachment for deletion on save
            deletedAttachmentIDs.insert(abstract.id)
        } else {
            // Take away newly added attachment
            newAttachments.removeAll { $0.id == abstract.id }
        }
    }

    non-public func processScannedImages(_ pictures: [UIImage]) {
        for (index, picture) in pictures.enumerated() {
            if let information = picture.jpegData(compressionQuality: 0.8) {
                let id = UUID()
                let filename = "scan_(attachmentSummaries.rely + index + 1).jpg"
                let mimeType = "picture/jpeg"

                // Add to newAttachments (with information) for saving
                let attachment = Attachment(filename: filename, information: information, mimeType: mimeType)
                attachment.id = id
                newAttachments.append(attachment)

                // Add abstract for show
                attachmentSummaries.append(AttachmentSummary(id: id, filename: filename, mimeType: mimeType))
            }
        }
    }

    non-public func processSelectedPhotos(_ objects: [PhotosPickerItem]) {
        for merchandise in objects {
            Job {
                if let information = strive? await merchandise.loadTransferable(sort: Knowledge.self) {
                    await MainActor.run {
                        let id = UUID()
                        let filename = "photo_(attachmentSummaries.rely + 1).jpg"
                        let mimeType = "picture/jpeg"

                        // Add to newAttachments (with information) for saving
                        let attachment = Attachment(filename: filename, information: information, mimeType: mimeType)
                        attachment.id = id
                        newAttachments.append(attachment)

                        // Add abstract for show
                        attachmentSummaries.append(AttachmentSummary(id: id, filename: filename, mimeType: mimeType))
                    }
                }
            }
        }
        selectedPhotoItems = []
    }

    non-public func processSelectedFiles(_ urls: [URL]) {
        for url in urls {
            guard url.startAccessingSecurityScopedResource() else { proceed }
            defer { url.stopAccessingSecurityScopedResource() }

            if let information = strive? Knowledge(contentsOf: url) {
                let id = UUID()
                let filename = url.lastPathComponent
                let mimeType = url.pathExtension.lowercased() == "pdf" ? "utility/pdf" : "picture/jpeg"

                // Add to newAttachments (with information) for saving
                let attachment = Attachment(filename: filename, information: information, mimeType: mimeType)
                attachment.id = id
                newAttachments.append(attachment)

                // Add abstract for show
                attachmentSummaries.append(AttachmentSummary(id: id, filename: filename, mimeType: mimeType))
            }
        }
    }
}

#Preview {
    EditPolicyView(coverage: PolicyItem(
        identify: "Check Coverage",
        class: .insurance coverage,
        supplier: "Check Supplier",
        renewalDate: Date()
    ))
    .modelContainer(for: PolicyItem.self, inMemory: true)
    .environmentObject(AppSettings.shared)
}

Additionally A screenshot of the view working on an iPhone 17 Professional Max: Issues with keyboard responsiveness in edit sheet

I’m certain I’m doing one thing intensely silly and would be glad about assist from the neighborhood on this.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisment -
Google search engine

Most Popular

Recent Comments