Harden iOS app with audit fixes, UI consistency, and sheet race condition fixes
Applies verified fixes from deep audit (concurrency, performance, security, accessibility), standardizes CRUD form buttons to Add/Save pattern, removes .drawingGroup() that broke search bar TextFields, and converts vulnerable .sheet(isPresented:) + if-let patterns to safe presentation to prevent blank white modals. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -100,7 +100,6 @@ struct TaskFormView: View {
|
||||
if needsResidenceSelection, let residences = residences {
|
||||
Section {
|
||||
Picker(L10n.Tasks.property, selection: $selectedResidence) {
|
||||
Text(L10n.Tasks.selectProperty).tag(nil as ResidenceResponse?)
|
||||
ForEach(residences, id: \.id) { residence in
|
||||
Text(residence.name).tag(residence as ResidenceResponse?)
|
||||
}
|
||||
@@ -111,10 +110,6 @@ struct TaskFormView: View {
|
||||
}
|
||||
} header: {
|
||||
Text(L10n.Tasks.property)
|
||||
} footer: {
|
||||
Text(L10n.Tasks.required)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
.sectionBackground()
|
||||
}
|
||||
@@ -168,7 +163,7 @@ struct TaskFormView: View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
TextField(L10n.Tasks.titleLabel, text: $title)
|
||||
.focused($focusedField, equals: .title)
|
||||
.onChange(of: title) { newValue in
|
||||
.onChange(of: title) { _, newValue in
|
||||
updateSuggestions(query: newValue)
|
||||
}
|
||||
|
||||
@@ -190,7 +185,6 @@ struct TaskFormView: View {
|
||||
TextField(L10n.Tasks.descriptionOptional, text: $description, axis: .vertical)
|
||||
.lineLimit(3...6)
|
||||
.focused($focusedField, equals: .description)
|
||||
.keyboardDismissToolbar()
|
||||
} header: {
|
||||
Text(L10n.Tasks.taskDetails)
|
||||
} footer: {
|
||||
@@ -219,7 +213,7 @@ struct TaskFormView: View {
|
||||
Text(frequency.displayName).tag(frequency as TaskFrequency?)
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedFrequency) { newFrequency in
|
||||
.onChange(of: selectedFrequency) { _, newFrequency in
|
||||
// Clear interval days if not Custom frequency
|
||||
if newFrequency?.name.lowercased() != "custom" {
|
||||
intervalDays = ""
|
||||
@@ -231,7 +225,6 @@ struct TaskFormView: View {
|
||||
TextField(L10n.Tasks.customInterval, text: $intervalDays)
|
||||
.keyboardType(.numberPad)
|
||||
.focused($focusedField, equals: .intervalDays)
|
||||
.keyboardDismissToolbar()
|
||||
}
|
||||
|
||||
DatePicker(L10n.Tasks.dueDate, selection: $dueDate, displayedComponents: .date)
|
||||
@@ -266,7 +259,6 @@ struct TaskFormView: View {
|
||||
.focused($focusedField, equals: .estimatedCost)
|
||||
}
|
||||
.sectionBackground()
|
||||
.keyboardDismissToolbar()
|
||||
|
||||
if let errorMessage = viewModel.errorMessage {
|
||||
Section {
|
||||
@@ -290,11 +282,20 @@ struct TaskFormView: View {
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(L10n.Common.save) {
|
||||
Button(isEditMode ? L10n.Common.save : L10n.Common.add) {
|
||||
submitForm()
|
||||
}
|
||||
.disabled(!canSave || viewModel.isLoading || isLoadingLookups)
|
||||
}
|
||||
|
||||
ToolbarItemGroup(placement: .keyboard) {
|
||||
Spacer()
|
||||
Button("Done") {
|
||||
focusedField = nil
|
||||
}
|
||||
.foregroundColor(Color.appPrimary)
|
||||
.fontWeight(.medium)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
// Track screen view for new tasks
|
||||
@@ -306,17 +307,17 @@ struct TaskFormView: View {
|
||||
setDefaults()
|
||||
}
|
||||
}
|
||||
.onChange(of: dataManager.lookupsInitialized) { initialized in
|
||||
.onChange(of: dataManager.lookupsInitialized) { _, initialized in
|
||||
if initialized {
|
||||
setDefaults()
|
||||
}
|
||||
}
|
||||
.onChange(of: viewModel.taskCreated) { created in
|
||||
.onChange(of: viewModel.taskCreated) { _, created in
|
||||
if created {
|
||||
isPresented = false
|
||||
}
|
||||
}
|
||||
.onChange(of: viewModel.errorMessage) { errorMessage in
|
||||
.onChange(of: viewModel.errorMessage) { _, errorMessage in
|
||||
if let errorMessage = errorMessage, !errorMessage.isEmpty {
|
||||
errorAlert = ErrorAlertInfo(message: errorMessage)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user