Dako

How Orange Lantern is built

2026-09-28 · technical note

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:

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:

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.