monotasker

Monotasker — plan

Canonical reference for what Monotasker is, how it works, and what’s left to build. Planning artifacts and historical specs live in docs/superpowers/.

Links: README


What’s next

Animations — v1.1 priority

All animations must gate on accessibilityReduceMotion (crossfade fallback). Gestures are a separate concern — animations should stand alone before gestures are layered on. The stack is honest: 1 task = 1 visible card, 2 tasks = 2, etc. (capped at a reasonable maximum). Animations should reflect the actual stack depth, so completing the last task reveals nothing, and shuffling with 2 cards shows both.

The system uses five motion primitives, each owned by one action:

View and behavior refinement


Deferred roadmap

Sections smoke test

Before implementing any sections-aware behavior, verify what EventKit returns from a sectioned list.

  1. In Reminders.app, add sections to the Monotasker list and add tasks inside each.
  2. Run Monotasker and shuffle several times — note whether section header names appear as tasks.
  3. Document findings in EventKitRemindersService for future contributors.

Manual test cases

Scene lifecycle manual tests

These require a physical device (or simulator with real permission flow). Each test is listed with setup, steps, and expected result. Run after any change to AppViewModel, MonotaskerApp, or EventKit interaction.

T1 — Grant permission from Settings (was permissionDenied)

  1. Fresh install or revoke Reminders access in Settings → Monotasker → Reminders = None.
  2. Launch Monotasker. Tap the onboarding checkbox → deny permission when prompted.
  3. Confirm app shows the ghost-card “Reminders access needed” screen.
  4. Without killing the app, open Settings → Monotasker → Reminders → set to Full Access.
  5. Return to Monotasker.

T2 — Revoke permission while app is in use

  1. Launch Monotasker with full Reminders access. Confirm a task is visible.
  2. Without killing the app, open Settings → Monotasker → Reminders → set to None.
  3. Return to Monotasker.

T3 — Return from Reminders.app after editing a task

  1. Launch Monotasker. Note the task title shown.
  2. Without killing the app, open Reminders.app and change the title of that task.
  3. Return to Monotasker.

T4 — Return from Reminders.app after deleting the current task

  1. Launch Monotasker with ≥2 tasks. Note the task shown.
  2. Open Reminders.app and delete that task (not all tasks).
  3. Return to Monotasker.

T5 — Return from Reminders.app after deleting the entire list

  1. Launch Monotasker. Confirm a task is visible.
  2. Open Reminders.app and delete the Monotasker list entirely.
  3. Return to Monotasker.

T6 — Undo window survives a brief background

  1. Launch Monotasker with ≥2 tasks.
  2. Tap Trash on a task — the undo toast appears (4-second window).
  3. Immediately home-screen the app and wait ~1 second, then return.
  4. Observe whether the undo toast is still showing or has committed.

T7 — EKEventStoreChanged fires after iCloud sync

  1. On Device A, launch Monotasker with a shared iCloud Reminders list.
  2. On Device B (or iCloud web), add a task to the same list.
  3. Wait for sync to propagate, or wait for Device A to receive the notification.

VoiceOver manual tests

These require a physical device with VoiceOver enabled (Settings → Accessibility → VoiceOver). Run after any change to view structure, accessibility labels, or hints. Enable VoiceOver before launching the app; use single-finger swipe right/left to move focus, double-tap to activate.

V1 — Onboarding traversal

  1. Fresh install (or revoke + relaunch). VoiceOver should land on the card.

V2 — Permission denied screen

  1. Deny permission at the onboarding prompt.

V3 — Focused task screen traversal

  1. With a task visible, swipe through all elements.

V4 — Complete and trash with undo (2+ tasks)

  1. With ≥2 tasks, double-tap Complete.
  1. Repeat with Trash.

V5 — Add task

  1. Double-tap the add button (pencil / below card).

V6 — Inline edit

  1. With a task visible, double-tap the edit button (pencil, lower-right of card).

V7 — List picker

  1. Double-tap the list picker button in the nav bar.

V8 — Empty list state

  1. Switch to a list with no tasks.

V9 — Large text with VoiceOver

  1. Set text size to maximum (Settings → Accessibility → Display & Text Size → Larger Text → drag to max), then enable VoiceOver.

Done


Reference

Decisions locked

Phase state machine

The happy path runs straight down the center: launch → permission check → list check → load pool → selection check → show task.

%%{init: {'flowchart': {'curve': 'basis', 'padding': 12}}}%%
flowchart TB
  Launch([Launch])
  Auth{Access OK?}
  ListCheck{List resolved?}
  LoadPool[Load pool]
  PoolCheck{Pool non-empty?}
  SelCheck{Selection valid?}
  ShowTask[Show task]

  Launch --> Auth
  Auth -->|full access| ListCheck
  ListCheck -->|yes| LoadPool
  LoadPool --> PoolCheck
  PoolCheck -->|yes| SelCheck
  SelCheck -->|yes| ShowTask

  Onboarding[Onboarding card]
  Instructions[Permission instructions]
  Auth -->|undetermined| Onboarding
  Onboarding -->|checkbox tap → granted| ListCheck
  Onboarding -->|checkbox tap → denied| Instructions
  Auth -->|denied / write-only| Instructions

  SetupList[List picker sheet]
  ListCheck -->|no| SetupList
  SetupList --> ListCheck

  EmptyState[Empty list]
  PoolCheck -->|no| EmptyState
  EmptyState -->|added task| LoadPool

  PickRandom[Pick at random]
  SelCheck -->|no| PickRandom
  PickRandom --> ShowTask

  AddSheet[Add task]
  ShowTask -->|complete / trash| LoadPool
  ShowTask -->|add| AddSheet
  AddSheet --> LoadPool

  Shuffle[Shuffle]
  ShowTask -->|shuffle| Shuffle
  Shuffle --> ShowTask
  ShowTask -->|inline edit| ShowTask
  ShowTask -->|switch list| SetupList

Diagram notes:

List resolution (zoomed in)

Reached after permission granted, when the stored list vanished, or when the user taps the list picker.

%%{init: {'flowchart': {'curve': 'basis', 'padding': 12}}}%%
flowchart TB
  Enter([Enter setup])
  StoredId{Stored ID valid?}
  NameMatch{Named Monotasker?}
  Toast["Toast: We found your Monotasker list!"]
  Picker[List picker sheet]
  Persist[Persist list id]
  Exit([Return to main flow])

  Enter --> StoredId
  StoredId -->|yes| Persist
  StoredId -->|no| NameMatch
  NameMatch -->|yes| Toast
  Toast --> Persist
  NameMatch -->|no| Picker
  Picker --> Persist
  Persist --> Exit

Architecture

Random selection

UniformRandomTopLevelPolicy implements uniform random choice with optional “excluding” id for shuffle. When excluding removes all candidates (single-task pool), the policy falls back to the full pool and the UI shows the “only one task” flow.

Add-task surfacing rule

Behavior depends on pool size when add started:

Implemented via poolSizeWhenAddOpened in AppViewModel.

Visual design

Source layout

Directory Purpose
Monotasker/App/ @main entry point, AppConfig
Monotasker/Models/ ReminderTask — domain model wrapping EKReminder
Monotasker/Services/ RemindersService protocol + EventKit/mock implementations
Monotasker/State/ AppViewModel, SelectionStore
Monotasker/Selection/ UniformRandomTopLevelPolicy
Monotasker/Views/ All SwiftUI views
Monotasker/Resources/ DesignColors, asset catalogs
MonotaskerTests/ Unit tests (selection policy, selection store, view model)

Renaming the app

  1. Update CFBundleDisplayName in Info.plist or via project.yml.
  2. Optionally change bundle id / target name in project.yml.
  3. Run xcodegen generate.
  4. Existing installs keep their chosen list id; new installs see the new default list name.

Maintenance