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:
Trey t
2026-03-06 09:59:56 -06:00
parent 61ab95d108
commit 9c574c4343
76 changed files with 824 additions and 971 deletions

View File

@@ -40,13 +40,7 @@ struct CompleteTaskIntent: AppIntent {
func perform() async throws -> some IntentResult {
print("CompleteTaskIntent: Starting completion for task \(taskId)")
// Mark task as pending completion immediately (optimistic UI)
WidgetActionManager.shared.markTaskPendingCompletion(taskId: taskId)
// Reload widget immediately to update task list and stats
WidgetCenter.shared.reloadTimelines(ofKind: "Casera")
// Get auth token and API URL from shared container
// Check auth BEFORE marking pending if auth fails the task should remain visible
guard let token = WidgetActionManager.shared.getAuthToken() else {
print("CompleteTaskIntent: No auth token available")
WidgetCenter.shared.reloadTimelines(ofKind: "Casera")
@@ -59,6 +53,12 @@ struct CompleteTaskIntent: AppIntent {
return .result()
}
// Mark task as pending completion (optimistic UI) only after auth is confirmed
WidgetActionManager.shared.markTaskPendingCompletion(taskId: taskId)
// Reload widget immediately to update task list and stats
WidgetCenter.shared.reloadTimelines(ofKind: "Casera")
// Make API call to complete the task
let success = await WidgetAPIClient.quickCompleteTask(
taskId: taskId,

View File

@@ -12,7 +12,5 @@ import SwiftUI
struct CaseraBundle: WidgetBundle {
var body: some Widget {
Casera()
CaseraControl()
CaseraLiveActivity()
}
}

View File

@@ -1,77 +0,0 @@
//
// CaseraControl.swift
// Casera
//
// Created by Trey Tartt on 11/5/25.
//
import AppIntents
import SwiftUI
import WidgetKit
struct CaseraControl: ControlWidget {
static let kind: String = "com.example.casera.Casera.Casera"
var body: some ControlWidgetConfiguration {
AppIntentControlConfiguration(
kind: Self.kind,
provider: Provider()
) { value in
ControlWidgetToggle(
"Start Timer",
isOn: value.isRunning,
action: StartTimerIntent(value.name)
) { isRunning in
Label(isRunning ? "On" : "Off", systemImage: "timer")
}
}
.displayName("Timer")
.description("A an example control that runs a timer.")
}
}
extension CaseraControl {
struct Value {
var isRunning: Bool
var name: String
}
struct Provider: AppIntentControlValueProvider {
func previewValue(configuration: TimerConfiguration) -> Value {
CaseraControl.Value(isRunning: false, name: configuration.timerName)
}
func currentValue(configuration: TimerConfiguration) async throws -> Value {
let isRunning = true // Check if the timer is running
return CaseraControl.Value(isRunning: isRunning, name: configuration.timerName)
}
}
}
struct TimerConfiguration: ControlConfigurationIntent {
static let title: LocalizedStringResource = "Timer Name Configuration"
@Parameter(title: "Timer Name", default: "Timer")
var timerName: String
}
struct StartTimerIntent: SetValueIntent {
static let title: LocalizedStringResource = "Start a timer"
@Parameter(title: "Timer Name")
var name: String
@Parameter(title: "Timer is running")
var value: Bool
init() {}
init(_ name: String) {
self.name = name
}
func perform() async throws -> some IntentResult {
// Start the timer
return .result()
}
}

View File

@@ -1,80 +0,0 @@
//
// CaseraLiveActivity.swift
// Casera
//
// Created by Trey Tartt on 11/5/25.
//
import ActivityKit
import WidgetKit
import SwiftUI
struct CaseraAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
// Dynamic stateful properties about your activity go here!
var emoji: String
}
// Fixed non-changing properties about your activity go here!
var name: String
}
struct CaseraLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: CaseraAttributes.self) { context in
// Lock screen/banner UI goes here
VStack {
Text("Hello \(context.state.emoji)")
}
.activityBackgroundTint(Color.cyan)
.activitySystemActionForegroundColor(Color.black)
} dynamicIsland: { context in
DynamicIsland {
// Expanded UI goes here. Compose the expanded UI through
// various regions, like leading/trailing/center/bottom
DynamicIslandExpandedRegion(.leading) {
Text("Leading")
}
DynamicIslandExpandedRegion(.trailing) {
Text("Trailing")
}
DynamicIslandExpandedRegion(.bottom) {
Text("Bottom \(context.state.emoji)")
// more content
}
} compactLeading: {
Text("L")
} compactTrailing: {
Text("T \(context.state.emoji)")
} minimal: {
Text(context.state.emoji)
}
.widgetURL(URL(string: "http://www.apple.com"))
.keylineTint(Color.red)
}
}
}
extension CaseraAttributes {
fileprivate static var preview: CaseraAttributes {
CaseraAttributes(name: "World")
}
}
extension CaseraAttributes.ContentState {
fileprivate static var smiley: CaseraAttributes.ContentState {
CaseraAttributes.ContentState(emoji: "😀")
}
fileprivate static var starEyes: CaseraAttributes.ContentState {
CaseraAttributes.ContentState(emoji: "🤩")
}
}
#Preview("Notification", as: .content, using: CaseraAttributes.preview) {
CaseraLiveActivity()
} contentStates: {
CaseraAttributes.ContentState.smiley
CaseraAttributes.ContentState.starEyes
}

View File

@@ -10,6 +10,28 @@ import SwiftUI
import AppIntents
// MARK: - Date Formatting Helper
/// Cached formatters to avoid repeated allocation in widget rendering
private enum WidgetDateFormatters {
static let dateOnly: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "yyyy-MM-dd"
return f
}()
static let iso8601WithFractional: ISO8601DateFormatter = {
let f = ISO8601DateFormatter()
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return f
}()
static let iso8601: ISO8601DateFormatter = {
let f = ISO8601DateFormatter()
f.formatOptions = [.withInternetDateTime]
return f
}()
}
/// Parses date strings in either yyyy-MM-dd or ISO8601 (RFC3339) format
/// and returns a user-friendly string like "Today" or "in X days"
private func formatWidgetDate(_ dateString: String) -> String {
@@ -17,20 +39,15 @@ private func formatWidgetDate(_ dateString: String) -> String {
var date: Date?
// Try parsing as yyyy-MM-dd first
let dateOnlyFormatter = DateFormatter()
dateOnlyFormatter.dateFormat = "yyyy-MM-dd"
date = dateOnlyFormatter.date(from: dateString)
date = WidgetDateFormatters.dateOnly.date(from: dateString)
// Try parsing as ISO8601 (RFC3339) if that fails
if date == nil {
let isoFormatter = ISO8601DateFormatter()
isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
date = isoFormatter.date(from: dateString)
date = WidgetDateFormatters.iso8601WithFractional.date(from: dateString)
// Try without fractional seconds
if date == nil {
isoFormatter.formatOptions = [.withInternetDateTime]
date = isoFormatter.date(from: dateString)
date = WidgetDateFormatters.iso8601.date(from: dateString)
}
}
@@ -179,9 +196,11 @@ struct Provider: AppIntentTimelineProvider {
let tasks = CacheManager.getUpcomingTasks()
let isInteractive = WidgetActionManager.shared.shouldShowInteractiveWidget()
// Update every 30 minutes (more frequent for interactive widgets)
// Use a longer refresh interval during overnight hours (11pm-6am)
let currentDate = Date()
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 30, to: currentDate)!
let hour = Calendar.current.component(.hour, from: currentDate)
let refreshMinutes = (hour >= 23 || hour < 6) ? 120 : 30
let nextUpdate = Calendar.current.date(byAdding: .minute, value: refreshMinutes, to: currentDate)!
let entry = SimpleEntry(
date: currentDate,
configuration: configuration,