How Orange Lantern is built
Orange Lantern is a small iOS app. It shows one small thing to do each day, with a painting. When you finish, you hold the painting and the light in it, a window, the sun or a lamp, comes on. This is how it is built, as of today's repository.
Structure
The app is SwiftUI on iOS 17. The Xcode project has two targets, the app and a WidgetKit extension. Most logic lives in a local Swift package with two modules:
- LanternCore depends only on Foundation: the day key (a day turns over at 4 a.m. local time), energy tiers and their clock defaults, the draw engine, the content format and the reminder planner.
- LanternData adds SwiftData: versioned models, DayService (today's draw, tier changes, done, rest, undo), JSON backup and store recovery.
The package also builds for macOS, so swift test runs 100 tests on the Mac in a few seconds. A separate harness runs 8 storage integration tests against the app's own model code on macOS.
Data
Records live in a SwiftData store inside an App Group container, so the app and the widget read the same file. iCloud sync is off. Schema V1 is frozen; V2 adds one optional field with a lightweight migration. DayService turns autosave off and wraps every change in a transaction that rolls back on failure. A damaged store falls back to memory, with the original kept for export. Backup is a versioned JSON file; import validates the whole file before merging.
There is no network layer: no URLSession, no analytics, no server. The privacy manifest declares no tracking and no collected data.
The draw
Each tier draws one item per day, and the draw stays fixed. The engine skips items shown in the last 7 days and avoids repeating a painting style across today's tiers or the previous 3 days.
Lighting the painting
Holding the painting drives a small state machine:
- A tap under 0.23 seconds only nudges you to hold. Holding charges a glow for 0.8 seconds; letting go early lets it fade.
- At full charge the record is written. Only then does the light fly to the painting's light source (0.62 seconds) and bloom (1.15 seconds).
- At 30 percent bloom, the cat or snail changes pose. Undo dims the light over 0.9 seconds. Reduce Motion skips the flight.
The Done button and VoiceOver run the same engine as an automatic hold.
Paintings
Each item has one 2:3 portrait painting and one reaction patch: the small region that differs after the light comes on. A Python export script checks the masters, finds the changed box, feathers its edge and writes its normalized position into actions.json. The bundle holds 85 paintings, 85 patches and one rest-day painting as HEIC, about 20 MB in all. The paintings are 1024×1536; the export script caps output at 1440×2160 and never upscales. The launch plan lists 2400×3600 masters exported at 1200×1800. The paintings are made with AI image tools from written prompts kept in per-item folders.
Widget, reminders, text
A medium home-screen widget shows today's item with a Done button backed by an App Intent. A circular lock-screen widget shows the lamp. Reminders are local notifications, 30 days ahead, 8 p.m. by default, skipped once the day is done or rested.
The interface is in English, Simplified and Traditional Chinese from one string catalog. Each language gets one font, subset to the characters the content uses: 827, 834 and 102. The three files total about 680 KB. A fontTools script cuts them, renames them to clear OFL reserved names, and checks the exported glyphs against the content.
Debug builds
Debug builds accept launch arguments that Release builds leave out: -demoTier 1|2|3, -demoLight 1, -demoRest 1, -demoTab review and -trialArt. They let us screenshot every state in the simulator.