Проблемы с реагированием клавиатуры на листе редактирования.IOS

Программируем под IOS
Ответить
Anonymous
 Проблемы с реагированием клавиатуры на листе редактирования.

Сообщение Anonymous »

Я создаю приложение на Swift UI и Swift 6 для iOS 26. Я использую SwiftData для хранения контента. У меня есть лист редактирования, с которым у меня возникают проблемы.
Когда лист загружается, пользователь не может нажать TextField для немедленного редактирования; для появления клавиатуры требуется несколько нажатий, и поэтому текст в поле выделяется для вырезания, копирования и вставки. Я занимался этим уже несколько дней, даже использовал искусственный интеллект, чтобы посмотреть, сможет ли он помочь, и не продвинулся дальше.
Мой код EditPolicyView.swift:

Код: Выделить всё

//
//  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)
}
Также скриншот представления, запущенного на iPhone 17 Pro Max:
Изображение

Я уверен, что делаю что-то очень глупое, и был бы благодарен сообществу за помощь в этом.

Подробнее здесь: https://stackoverflow.com/questions/798 ... edit-sheet
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «IOS»