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:
@@ -586,6 +586,7 @@ enum L10n {
|
||||
// MARK: - Common
|
||||
enum Common {
|
||||
static var save: String { String(localized: "common_save") }
|
||||
static var add: String { String(localized: "common_add") }
|
||||
static var cancel: String { String(localized: "common_cancel") }
|
||||
static var delete: String { String(localized: "common_delete") }
|
||||
static var edit: String { String(localized: "common_edit") }
|
||||
|
||||
@@ -44,7 +44,7 @@ struct ViewStateHandler<Content: View>: View {
|
||||
content
|
||||
}
|
||||
}
|
||||
.onChange(of: error) { errorMessage in
|
||||
.onChange(of: error) { _, errorMessage in
|
||||
if let errorMessage = errorMessage, !errorMessage.isEmpty {
|
||||
errorAlert = ErrorAlertInfo(message: errorMessage)
|
||||
}
|
||||
@@ -93,7 +93,7 @@ private struct ErrorHandlerModifier: ViewModifier {
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.onChange(of: error) { errorMessage in
|
||||
.onChange(of: error) { _, errorMessage in
|
||||
if let errorMessage = errorMessage, !errorMessage.isEmpty {
|
||||
errorAlert = ErrorAlertInfo(message: errorMessage)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,12 @@ import WidgetKit
|
||||
final class WidgetActionProcessor {
|
||||
static let shared = WidgetActionProcessor()
|
||||
|
||||
/// Maximum number of retry attempts per action before giving up
|
||||
private static let maxRetries = 3
|
||||
|
||||
/// Tracks retry counts by action description (taskId)
|
||||
private var retryCounts: [Int: Int] = [:]
|
||||
|
||||
private init() {}
|
||||
|
||||
/// Check if there are pending widget actions to process
|
||||
@@ -65,23 +71,38 @@ final class WidgetActionProcessor {
|
||||
|
||||
if result is ApiResultSuccess<TaskCompletionResponse> {
|
||||
print("WidgetActionProcessor: Task \(taskId) completed successfully")
|
||||
// Remove the processed action
|
||||
// Remove the processed action and clear pending state
|
||||
WidgetDataManager.shared.removeAction(action)
|
||||
// Clear pending state for this task
|
||||
WidgetDataManager.shared.clearPendingState(forTaskId: taskId)
|
||||
retryCounts.removeValue(forKey: taskId)
|
||||
// Refresh tasks to update UI
|
||||
await refreshTasks()
|
||||
} else if let error = ApiResultBridge.error(from: result) {
|
||||
print("WidgetActionProcessor: Failed to complete task \(taskId): \(error.message)")
|
||||
// Remove action to avoid infinite retries
|
||||
WidgetDataManager.shared.removeAction(action)
|
||||
WidgetDataManager.shared.clearPendingState(forTaskId: taskId)
|
||||
handleRetryOrDiscard(taskId: taskId, action: action, reason: error.message)
|
||||
}
|
||||
} catch {
|
||||
print("WidgetActionProcessor: Error completing task \(taskId): \(error)")
|
||||
// Remove action to avoid retries on error
|
||||
handleRetryOrDiscard(taskId: taskId, action: action, reason: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
/// Increment retry count; discard action only after maxRetries.
|
||||
/// On failure, clear pending state so the task reappears in the widget.
|
||||
private func handleRetryOrDiscard(taskId: Int, action: WidgetDataManager.WidgetAction, reason: String) {
|
||||
let attempts = (retryCounts[taskId] ?? 0) + 1
|
||||
retryCounts[taskId] = attempts
|
||||
|
||||
if attempts >= Self.maxRetries {
|
||||
print("WidgetActionProcessor: Task \(taskId) failed after \(attempts) attempts (\(reason)). Discarding action.")
|
||||
WidgetDataManager.shared.removeAction(action)
|
||||
WidgetDataManager.shared.clearPendingState(forTaskId: taskId)
|
||||
retryCounts.removeValue(forKey: taskId)
|
||||
} else {
|
||||
print("WidgetActionProcessor: Task \(taskId) attempt \(attempts)/\(Self.maxRetries) failed (\(reason)). Keeping for retry.")
|
||||
// Clear pending state so the task is visible in the widget again,
|
||||
// but keep the action so it will be retried next time the app becomes active.
|
||||
WidgetDataManager.shared.clearPendingState(forTaskId: taskId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -222,10 +222,14 @@ final class WidgetDataManager {
|
||||
fileQueue.async {
|
||||
// Load actions within the serial queue to avoid race conditions
|
||||
var actions: [WidgetAction]
|
||||
if FileManager.default.fileExists(atPath: fileURL.path),
|
||||
let data = try? Data(contentsOf: fileURL),
|
||||
let decoded = try? JSONDecoder().decode([WidgetAction].self, from: data) {
|
||||
actions = decoded
|
||||
if FileManager.default.fileExists(atPath: fileURL.path) {
|
||||
do {
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
actions = try JSONDecoder().decode([WidgetAction].self, from: data)
|
||||
} catch {
|
||||
print("WidgetDataManager: Failed to decode pending actions: \(error)")
|
||||
actions = []
|
||||
}
|
||||
} else {
|
||||
actions = []
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user