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:
@@ -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,
|
||||
|
||||
@@ -12,7 +12,5 @@ import SwiftUI
|
||||
struct CaseraBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
Casera()
|
||||
CaseraControl()
|
||||
CaseraLiveActivity()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user