← Back to warehouse

Time Left Review

SwiftUI iOS app — Countdown / countup timer with events, habits, and custom tracking.
Review date: May 2026

UI / UX Screenshots

Key insight: The app uses a card-heavy layout with bold typography for countdown values. "Add Event" uses a clean bottom sheet with segmented controls for mode (countdown / countup). The main list shows progress bars and days-remaining chips. Settings is minimal but functional. Visual hierarchy is clear — primary actions use high-contrast fills.
3
Critical Issues
7
Warnings
5
Improvements
3
Architecture Wins

Critical Issues

Critical Timer Retain Cycle — Memory Leak on Every Event MainHomeView.swift:52

The Timer created in .onAppear captures self (the view struct is re-created, but the closure holds the @StateObject or @State reference). More critically, the timer is never invalidated when the view disappears — it fires forever even on background screens.

@State private var timer: Timer? .onAppear { timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in updateCountdown() // Captures self implicitly } } // No .onDisappear { timer?.invalidate() }

Fix: Use .onReceive(Timer.publish(every: 1, on: .main, in: .common).autoconnect()) instead of manual Timer. It auto-cancels when view leaves hierarchy. Or add .onDisappear { timer?.invalidate(); timer = nil }.

Critical Date Formatter Rebuilt 1x/Second — 100% CPU on Countdown Screen MainHomeView.swift:28-30

Inside the timer closure, you construct a new DateComponentsFormatter every tick. These are expensive. On a device with 20 events, that's 20 formatters * 1Hz = 20 allocations/second just for display strings.

Text("Time left: \(DateComponentsFormatter().string(from: now, to: eventDate)!)")

Fix: Pre-create formatter as a static constant, or cache formatted strings and only update when the displayed value changes (every second is fine, but reuse the formatter instance).

Critical Force Unwrap on Countdown Formatting MainHomeView.swift:28

DateComponentsFormatter().string(from:to:) returns optional. Force unwrapping crashes if inputs are invalid (e.g., from > to, or nil calendar).

DateComponentsFormatter().string(from: now, to: eventDate)!

Fix: guard let timeLeft = formatter.string(from: now, to: eventDate) else { return "—" }

Warnings

Warning ContentView Is Empty Shell — All Logic in MainHomeView ContentView.swift:1-35

ContentView contains a placeholder title and "Current Time" text, then MainHomeView is presumably the real entry point. This dead code confuses the navigation stack and adds an unnecessary layer.

Fix: Delete ContentView and make MainHomeView the root. Or make ContentView a lightweight router that switches between Home / Paywall / Onboarding based on state.

Warning PaywallView Has No Purchase Error Handling PaywallView.swift:67-72

Identical to LeaveHack — errors are silently swallowed. The purchase button just shows a spinner forever if the network fails.

Fix: Same fix: publish errors and show an alert.

Warning EnvironmentObject Assumed Present — Crash Risk MainHomeView.swift:10

Using @EnvironmentObject var appState: AppState without a fallback. If the parent doesn't inject it, the app crashes at runtime with a cryptic SwiftUI error.

@EnvironmentObject var appState: AppState // Crashes if missing

Fix: Use @ObservedObject var appState: AppState passed explicitly, or inject in the App .environmentObject(appState) and audit all previews to inject a mock.

Warning Duplicate AppState Definition ContentView.swift:36 / MainHomeView.swift:38

AppState is defined in both files (same struct, duplicated). This compiles because Swift allows same-name structs in different files, but it's a maintenance bomb — change one, forget the other.

Fix: Move AppState to its own file (e.g., AppState.swift) and import everywhere. Delete the duplicates.

Warning Unused Imports (WebKit, UIKit) ContentView.swift:7-8

Importing WebKit and UIKit in a pure SwiftUI file adds compile time and binary bloat for no benefit. No WebView or UIKit bridging is present.

Fix: Remove unused imports. Audit all files.

Warning No Persistence for Events Models.swift (inferred)

Events appear to be in-memory only. Kill the app, lose all custom countdowns. For a "tracker" app, this is a fundamental UX gap.

Fix: Add SwiftData (@Model) or Core Data persistence. At minimum, UserDefaults + Codable for simple event arrays.

Warning No Background Refresh / Notification Scheduling AppDelegate (inferred missing)

A countdown app that doesn't notify users when an event happens is incomplete. No UNUserNotificationCenter integration found.

Fix: Schedule local notifications for event deadlines. Request permission on first event creation.

Architecture & Patterns

Win EventMode Enum Is Clean ContentView.swift:45-49

Good: Simple enum with CaseIterable for modes. Could be used in a Picker with ForEach(EventMode.allCases).

Pattern CustomEvent Struct Needs Equatable / Identifiable ContentView.swift:41-44

CustomEvent has an id but doesn't conform to Identifiable. This means SwiftUI lists need \.id explicitly, and animations (insert/delete/move) won't auto-apply.

struct CustomEvent { var id: String; var mode: EventMode } struct CustomEvent: Identifiable { let id: String; var mode: EventMode }

Fix: Add Identifiable and make id a let (immutable identity).

Pattern MainHomeView Is Both Container and Leaf MainHomeView.swift:1-36

The view mixes timer logic, animation state, and UI layout. Consider splitting into CountdownListView (UI) and CountdownViewModel (timer + formatting logic).

Priority Action List

  1. 1
    Fix timer memory leak — Switch to .onReceive(Timer.publish(...)) or add .onDisappear { invalidate() }. This is a crash/performance bug.
  2. 2
    Cache DateComponentsFormatter — Create once, reuse. 20 allocations/sec is wasteful.
  3. 3
    Deduplicate AppState — Move to single file, delete copies in ContentView and MainHomeView.
  4. 4
    Add event persistence — SwiftData or Codable + UserDefaults. Events vanish on app kill.
  5. 5
    Delete or use ContentView — Currently dead code. Make it the router or remove it.
  6. 6
    Fix force unwraps — All ! in view layer. Add guards with sensible fallbacks.
  7. 7
    Add local notifications — Core feature gap for a countdown app.
  8. 8
    Conform CustomEvent to Identifiable — Enables list animations and cleaner ForEach.
  9. 9
    Audit and remove unused imports — WebKit, UIKit where not needed.
  10. 10
    Extract ViewModel — Separate timer/formatting logic from SwiftUI view.