Probleme mit der Reaktionsfähigkeit der Tastatur im BearbeitungsblattIOS

Programmierung für iOS
Anonymous
 Probleme mit der Reaktionsfähigkeit der Tastatur im Bearbeitungsblatt

Post by Anonymous »

Ich erstelle eine App in Swift UI und Swift 6 für iOS 26. Ich verwende SwiftData zum Speichern von Inhalten. Ich habe ein Bearbeitungsblatt, mit dem ich Probleme habe.
Wenn das Blatt geladen wird, kann der Benutzer nicht auf das TextFeld tippen, um es sofort zu bearbeiten. Es sind mehrere Berührungen erforderlich, bis die Tastatur angezeigt wird. Dadurch wird der Text im Feld zum Ausschneiden, Kopieren und Einfügen hervorgehoben. Ich beschäftige mich seit Tagen damit und nutze sogar KI, um zu sehen, ob es helfen kann, aber ich komme nicht weiter.
Mein EditPolicyView.swift-Code:

Code: Select all

//
//  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 = []  // 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)
}
Außerdem ein Screenshot der Ansicht, die auf einem iPhone 17 Pro Max ausgeführt wird:
Image

Ich bin mir sicher, dass ich etwas äußerst Dummes mache und wäre für die Hilfe der Community dabei dankbar.

Quick Reply

Change Text Case: 
   
  • Similar Topics
    Replies
    Views
    Last post