[ SEP 4, 2026 ]
Habitizer Case Study: What does an end-to-end iOS App Build look like?
Author Pascal Rettig | Written by a Human
Habitizer was a small itch-to-scratch project that I built as a case study to see what an end-to-end build for an intentionally simple SwiftUI iOS app would look like.
Here's a video of what the e2e test suite ended up looking like:
Process
The process for this app was:
- The core idea was written up in a few paragraphs
- The design was mocked and iterated on in Claude design
- A SwiftData-driven, layered architecture was written up as part of a bootstrapping of the project via a CLAUDE.md file.
- The overall process I've described elsewhere was used to actually build the project.
Claude skipped installing XcodeBuildMCP for building / testing and this wasn't caught until the next morning after review of the overnight stages review files that showed the agent wasn't actually running the app, just the test suite. This proved to be only a minor setback, but still annoying.
This was built back in the beginning of July using a slightly older version of the process I've outlined in other blog posts from a single design document. (Larger projects have used an Architecture → Phasing → Multiple Design Docs approach.) You can see the code for the project itself at https://github.com/cykod/habitizer
Human takeaways
- With the right Xcode Project setup and feedback loop, this project could have been one-shot - by far the longest part of the project was iterating on the UX in Claude Design (Claude still gets the UX wrong almost every time).
- XCodebuildMCP is a must for any iOS projects and it works great (You can supergateway and run the server on a OS X host while building in a container - I'd consider this safer than running on the host, but not 100% safe)
- I'll write more about layers in other posts - but keeping the actual domain logic pure and in its own phases means the core functionality of the app can built first and tested independently before the UI is merged in. This has worked well every time I've done it.
- A rule that the agent can only work on 1 layer at a time worked well here and resulted in better factored code that can be independently unit tested.
- Simplifying the architecture down (no server) made this much easier to build, even though SwiftData is still a bit of a quagmire.
AI-Generated Case Study
Below is an only-slightly-edited case study Claude wrote up based on session transcripts. All the text below here, except for the callout quotes, is AI-Generated.
TL;DR
| What | Habitizer, a personal iOS habit-schedule app: timed routines, a lock-screen Live Activity and widget to check tasks off, and a review screen with charts. |
| When | Wednesday July 1 to Friday July 3, 2026. 49.5 hours of calendar time. |
| Human time in the loop | About 12 hours of prompting activity across 12 sessions and 49 prompts (37 of them substantive). |
| Agent time | 15.4 hours of measured model activity — 3,245 assistant turns, 3,998 tool calls, 99 subagents. |
| Time the agent spent blocked | 16.7 hours across 17 pauses waiting for a human answer. More wall clock went to waiting than to working. |
| Output | 28 commits. 78 Swift files, 9,915 lines (3,275 of them tests). 157 unit tests, 9 UI tests. 20 markdown files, 6,948 lines of specs, designs, reviews and retros. |
| Process | 9 phases plus 6 polish phases. 9 of 9 gate reviews passed. 14 retros producing 65 findings, every one folded back into the project's own playbooks. |
| Cost | Ran on a Claude subscription. Estimated $607. 2.7M output tokens, 496M read from cache. |
| Effort split | Implementing was 23% of the spend. Reviewing, fixing and learning together were 37%; designing 21%; orchestrating the pipeline 18%. |
| Bugs the human found | One. It was at the process boundary the simulator gate could not see. |
| Biggest single time sink | The hand-created Xcode project: three pauses for manual target work cost 12.7 hours of wall clock and about ten minutes of actual human effort. |
The effort split has a slightly higher percentage on implementing than other projects. As project grow they tend to need more time for review and orchestration -Pascal
What was built
Habitizer is a personal tool. You define a routine once ("Get up 6:00, coffee 6:10, work out
7:00, kids' lunches 7:15, leave 7:50"), toggle it on, and the app nudges you at the start
time. As you go, you tap Complete or Skip on each task, ideally from the lock screen without opening the app. Afterwards, a review screen shows what you did and how far ahead of or behind each target you were.
The feature list, all shipped and gate-verified on the simulator:
- Schedule authoring. Two kinds of schedule: time-based (fires a start notification on chosen days) and on-demand (start whenever). Tasks are stored as offsets from a per-run anchor and always shown sorted by time.
- Active run. A dark, single-task screen with live ahead/behind status, Complete and Skip, a running-now banner to resume, an abandon action, and a completion summary.
- Start notifications. Idempotent scheduling, notify-only (the run starts when you engage, not when the alert fires).
- Live Activity and Dynamic Island. The current task with Complete and Skip buttons in place, kept in lockstep with the run.
- Home-screen widget with the same interactive buttons, backed by App Intents that write to an App-Group shared SwiftData store without launching the app.
- Review. Daily breakdown with diverging ahead/behind bars, a 7-day completion chart, a per-task average chart, and an on-time percentage badge on each schedule card.
- CloudKit-safe persistence. Every model follows the CloudKit rules from the first commit.
The design came from a Claude Design mockup vendored into the repo as the visual source of truth. The build was held to it phase by phase.
Architecture in one picture
The architecture doc, written on the first afternoon, fixed the shape everything else
followed: a pure, framework-free Domain inside an impure shell, and two processes sharing one store.
The Domain takes time as a parameter, never calls Date(), and imports only Foundation. A one-line grep in the lint script fails the build if anything under Domain/ imports SwiftData, SwiftUI, UserNotifications, ActivityKit or WidgetKit. That single invariant is the reason the final phase, where a widget extension runs the same advance logic as the app, landed without rearchitecting anything.
Timeline
Day 1, Wednesday: from a paragraph to six phases
At 12:02 the first prompt was a paragraph describing the app, ending “please come up with an initial CLAUDE.md document explaining the project.” The next two hours produced, in order: the project spec, a set of agent playbooks bootstrapped from the user’s other projects, an architecture doc with ten numbered decisions, and a nine-phase plan. One product change came in mid-stream (“the system should also support ad-hoc schedules”) and rippled through the architecture before any code existed. The Claude Design mockup was pulled in at 14:57.
Phase 1 started at 15:13 and needed the first human step at 15:18: creating the Xcode project by hand. The project landed in a scratch folder because nobody had said where to save it, and the agent relocated it. By 17:05 the walking skeleton was committed with a green gate. The gate review passed at 17:56.
At 17:59 the user typed /autobuild for phases 2 through 9 and walked away. Phases 2 through 6 committed at 18:45, 19:16, 21:01, 22:23 and 23:26 with no prompts in between: persistence, the pure domain, the authoring UI, the run screen, and the review charts. Five feature commits, each with its own design doc, four reviews, gate review and retro, in five and a half hours of unattended work.
The autobuild then stopped, because the next phases needed a widget extension target and an App Group capability that only Xcode can add.
Day 2, Thursday: the process boundary
At 06:51 the user asked what to name the widget target, did it, and at 06:55 typed “I’ve set up xcode - please continue.” That exchange took four minutes; the question that prompted it had been sitting unanswered since 23:27 the night before, so the day opened seven hours behind. Phase 7 (notifications) committed forty minutes later. Phase 8 (Live Activity) is recorded as four hours, but three of them were the pipeline idle from 08:20 to 11:20 waiting on a two-checkbox Xcode step: one shared file had to be a member of both targets. Phase 9 (widget and App Intents) is recorded as five hours, of which two and a half were the same thing at larger scale — fifteen files added to the widget target in one sitting, asked at 14:00 and answered at 16:24. The two “four and five hour phases” of the autobuild were, in model time, closer to one hour and two and a half.
At 16:44 Phase 9 was committed, gate-green. At 16:54 the user, running it on a real phone, reported the only bug a human found in the project: tapping Complete on the lock screen advanced the run, but the Live Activity card did not update. The first fix did not work. The user asked “Are you able to build and drive the simulator yourself?” and, at 17:05, pasted missing config for the XcodeBuildMCP CLI. The second fix worked and committed at 17:20.
That question changed the last third of the project. At 17:44 the user asked the agent to “run through the full app and all the different cycles you can” using the new tooling and write a design doc for what it found. Driving the app itself, the agent catalogued 3 blockers, 8 findings and 5 nits, including an on-time score that read 100% while you were skipping everything, a Review screen reachable only by an undiscoverable long-press, and a denied notification permission that silently broke the whole notify-to-start flow. It grouped them into six phases and the build started that evening — and died twenty minutes in, when a ten-minute network outage took the session down with no retry. Nobody noticed until morning (lesson 11).
Day 3, Friday: polish and honesty
The day began at 06:13 with “please rerun the four Phase A review agents” — typing back by hand the work the outage had eaten. From there it ran cleanly. Phase A changed the semantics of the two numbers a user actually reads, on-time percentage and streak, so skips count against you. Phase D added abandon, a supersede confirmation, and a real completion summary in place of a 1.1-second flash. B, C, E and F landed together: a one-tap Review button, a “notifications are off” banner, Live Activity staleness handling, and visual fixes. The last human step was vendoring the two brand fonts, a binary-asset change the agent cannot make, followed by a final commit at 13:35.
Where the human was needed
Of 49 prompts, 12 were session housekeeping. The substantive interventions fall into four groups:
- Direction (about 10): the initial spec, the ad-hoc schedules change, the architecture and phasing requests, importing the design, kicking off each build, asking for the self-driven beta review.
- Xcode steps (4): create the project, add the widget target and App Group, add cross-target file memberships, vendor the fonts.
Decisions escalated by a skill (3, recorded in the review artifacts): what happens to pending tasks at day rollover, whether a superseded run is kept, and which on-time definition to use. - A bug report (1): the Live Activity not updating on device.
Everything else was the agent asking itself questions and answering them in review documents.
But the interesting number is not how often the human was needed — it is how long the pipeline stood still waiting. The transcripts record 17 points where a skill stopped and asked, totalling 16.7 hours of dead wall clock against 15.4 hours of actual model activity. The distribution is brutally uneven:
| Asked | Blocked | What was being asked |
|---|---|---|
| Wed 23:27 | 7h 16m | "Phases 7–9 need manual Xcode capability/signing steps I can't do headless. How should I proceed?" (the human was asleep) |
| Thu 08:20 | 3h 00m | Add one shared file to both target memberships in Xcode |
| Thu 14:00 | 2h 24m | Add 15 files to the widget target membership |
| Thu 18:16 | 1h 34m | Should skipped tasks count against the on-time headline metric? |
| Thu 11:52 | 1h 28m | How should the app pick up the extension's cross-process write? |
| 12 others | 1h 05m total | Batch plans, scope confirmations, naming |
Only two of those — the on-time semantics and the cross-process read strategy — are questions a human genuinely had to answer. They cost 3 hours. The other 14 hours went to mechanical Xcode work and to a scheduling question the agent had already answered itself (it recommended "Continue" and then waited seven hours for permission to take its own advice).
AI may end up taking over the world just because it's angry at having to wait while we sleep -Pascal
What it cost
Transcripts for all twelve sessions were recorded, including the 99 subagent transcripts, and every assistant turn carries its token usage. Summing them, deduplicated by message id and priced at Anthropic’s list rates for Opus 4.8 (cache writes at 2x input, the rate for the one-hour cache TTL Claude Code uses):
| Main sessions | Subagents | Total | |
|---|---|---|---|
| Fresh input tokens | 269,420 | 693,906 | 963,326 |
| Output tokens | 994,885 | 1,674,517 | 2,669,402 |
| Cache-write tokens | 9,651,465 | 8,181,929 | 17,833,394 |
| Cache-read tokens | 279,108,966 | 217,096,771 | 496,205,737 |
| Assistant turns | 947 | 2,298 | 3,245 |
| Tool calls | 893 | 3,105 | 3,998 |
| API-equivalent cost | $262.28 | $235.25 | $497.53 |
I didn't check these numbers, I'd take them as roughly accurate -Pascal
96.4% of everything the model read came from cache. That is what a long agentic session with a stable system prompt and a growing set of project playbooks looks like: the same architecture doc, implementation spec and file tree are re-read on every turn, and almost all of it is a cache hit at a tenth of the input price. Without caching the same work would have cost roughly $2,650.
Subagents are half the bill. They produced 63% of the output tokens and 47% of the cost while the orchestrating sessions mostly dispatched and summarized. Any estimate built from the main transcripts alone would have been about half the truth.
If you want a cost story for your own project, keep the transcripts. Raise cleanupPeriodDays in Claude Code's settings before you start, and copy the directory out. Everything in this section is measured from files that Claude Code's 30-day cleanup deleted on day 61; only a backup made them recoverable. Without them the honest answer to "what did this cost" is a shrug.
Where the effort went, step by step
The same transcripts that give the cost also say which step of the pipeline spent it. Every
assistant turn can be attributed: subagent runs by the dispatch that launched them and by the agent-<id>.meta.json sidecar recording which skill asked for them, main-session turns by the skill that was running — tracked as a stack, because skills nest (/autobuild → /build → /autocommit) and a transcript records every entry and no return. Tracking a single "current skill" instead lets a 70-second commit inherit the two hours of building that follow it.
One caveat on the hours column. Agent-hours are each agent's own active span, summed, and four subagents plus the driver session ran at once, so they exceed the 15.4 hours of wall clock. The orchestration rows overlap their own subagent rows by construction. Turns, tool calls and dollars are strictly additive and reconcile to the totals in the previous section.
By role — what the work actually was
| Role | Where | Runs | Turns | Tool calls | Agent-hours | Output | Cost | Share |
|---|---|---|---|---|---|---|---|---|
| Implement | subagent | 15 | 995 | 1,195 | 4.39 | 811K | $114.32 | 23.0% |
| Design authoring | main | — | 300 | 293 | 3.69 | 412K | $93.20 | 18.7% |
| Build orchestration | main | — | 230 | 194 | 6.75 | 225K | $71.44 | 14.4% |
| Retro application | main | — | 196 | 189 | 1.80 | 189K | $55.07 | 11.1% |
| Functional review | subagent | 16 | 528 | 694 | 2.08 | 283K | $48.04 | 9.7% |
| Fix | subagent | 12 | 267 | 360 | 1.05 | 163K | $22.25 | 4.5% |
| Gate review | main | — | 69 | 74 | 0.36 | 49K | $19.92 | 4.0% |
| Code review | subagent | 15 | 179 | 321 | 0.87 | 133K | $18.18 | 3.7% |
| Retro write-up | subagent | 15 | 109 | 165 | 0.58 | 104K | $12.17 | 2.4% |
| Devil's-advocate review | subagent | 10 | 86 | 172 | 0.66 | 111K | $11.61 | 2.3% |
| Commit + changelog | main | — | 37 | 34 | 0.34 | 20K | $10.04 | 2.0% |
| Unstructured prompting | main | — | 90 | 86 | 0.87 | 65K | $8.72 | 1.8% |
| Architecture + security review | subagent | 15 | 125 | 171 | 0.41 | 62K | $8.57 | 1.7% |
| Tooling, housekeeping, exploration, pipeline | both | 1 | 34 | 50 | 0.34 | 42K | $4.00 | 0.8% |
| Total | 99 agents | 3,245 | 3,998 | 2.67M | $497.53 |
Writing the code was under a quarter of the work. Implementation is $114 of $498. The quality loop wrapped around it — reviewing, fixing and learning — is $184, or 37%, half again as much. Designing what to build is another 21%. For every dollar spent writing Swift, about $1.60 went to checking it and $0.90 to deciding what it should be. That ratio is not a law of nature; it is what /autobuild was configured to buy, and it is the number to argue about if you want a cheaper pipeline.
Reviewing is tool-heavy, not token-heavy. The 46 review runs made 1,260 tool calls, more than implementation's 1,195, while producing 35% fewer output tokens. Reviews build, launch, tap and screenshot; they don't write much. 694 of those calls belong to the functional review alone, which is the only step that drives the app.
The review fan-out is nearly free in wall clock. Four agents go out together after each
implement — functional review, code review, architecture/security review, and the phase retro — and they average 7.8, 3.5, 1.6 and 2.3 minutes of their own active time. Run in sequence that is about 15 minutes a phase; run in parallel it is about eight. Across fifteen batches the fan-out bought back roughly two hours of calendar time for exactly the same dollars.
Applying the lessons cost four and a half times more than learning them. Writing the retros was $12.17 of subagent time. /apply-retro — editing CLAUDE.md and the three playbooks so the next phase reads the fix — was $55.07. The compounding loop that this document credits for the build's stability is 13.5% of the entire bill. It is the single most under-appreciated line item here: cheap to generate insight, expensive to install it.
Cost per agent-hour varies five-fold, from $55/h for /gate-review down to $10/h for the unstructured day-one prompting and the build driver. Gate review is the shortest step in the pipeline (2.4 minutes) and one of the priciest, because it reads every review artifact for the phase and then re-runs the whole gate itself. Day one was cheap because there was barely any context to reason over yet. The bill tracks how much context a step holds, not how long it takes — which is the same fact as the 96.4% cache-read share, seen from the other end.
A fifth of the spend never touched the product. Orchestration — dispatching, collating agent reports, deciding what comes next, committing — is $92. That is the overhead of the multi-agent shape, and it is the price of the unattended overnight run that produced five phases while nobody was watching.
The longest single agent run in the project was the Phase 4 authoring-UI implementation at 48 minutes; Phase 9's widget and App Intents took 39, Phase 5's run experience 30. No review agent ran longer than 28 minutes, and the median agent run of any kind was 3.4 minutes.
The stumbling blocks, ranked by what they actually cost
Retro findings are self-reported, so they over-weight what felt hard and under-weight what was
merely slow. The transcripts give an independent measure. The gate — lint, zero-warning build, unit
tests, UI tests — ran 373 times and failed 114. Classifying each failure by cause, and setting
that beside the wall clock each problem consumed:
| Rank | Stumbling block | Failing gate runs | Wall clock lost | Nature |
|---|---|---|---|---|
| 1 | Hand-created Xcode project (target membership, capabilities) | 7 (6%) | 12h 40m across 3 pauses | Structural — no agent can cross it |
| 2 | Unattended-run fragility (a network blip ended the night) | — | 9h 07m | Operational — nothing retried |
| 3 | Lint and formatter disagreement | 53 (46%) | minutes each, ~50 times | Self-inflicted, entirely avoidable |
| 4 | Architecture invariant violations (D1 grep guard) | 15 (13%) | seconds each | The gate working as intended |
| 5 | XCUITest blind spots | 12 (11%) | 25m in one loop; recurred to day 3 | Knowledge — invisible failure modes |
| 6 | Swift 6 concurrency vs. ActivityKit | 7 (6%) | clustered in 2 phases | Genuinely hard; found a real bug |
| 7 | Toolchain/environment (swiftlint absent, sim name) | 2 (2%) | one-time | Setup cost |
| 8 | SwiftData/CloudKit | 1 (1%) | — | Loud once, then designed out |
| — | Unclassified / generic compile errors | 15 (13%) | — | Ordinary iteration |
Two things stand out. The problems that cost the most time are not the ones the retros wrote most about. The retros are full of SwiftData and Swift-6 lore — real, valuable, and worth the pages — but SwiftData broke the gate exactly once, because the CloudKit rules were written into the project spec before the first model was typed. Meanwhile the two largest costs, the Xcode seam and a dropped network connection, generated almost no findings at all, because neither is a coding problem and
the retro prompt asks about the code.
And nearly half of all gate failures were formatting. Not one of them was a defect. That is the cheapest thing on this list to fix, and it is lesson 8. The D1 architecture guard is the other surprise: the one-line grep that forbids framework imports under Domain/ stopped the build 15 times. It is the cheapest check in the gate and the third most frequent thing to catch something.
What worked
- The pure-core / impure-shell split, enforced by lint. Every hard problem in the project was in the shell. The Domain was written in half an hour on day 1 and barely changed.
- Devil's advocate before implementation. The single most valuable step in the pipeline. It caught the observation bug, the task-ordering race, the stale-context reflect and the
entitlement throw before any of them cost a debug cycle. - Reviews that run the app. Every functional review built, launched and drove the app in the simulator with narrated screenshots, using a temporary XCUITest harness early on and the XcodeBuildMCP CLI later. Fifty-eight screenshots survive in the review folders.
- A gate review that asks "was it exercised?" and re-runs the gate itself.
- Retro to playbook, every phase. The implementation playbook grew from conventions to a per-phase gotchas catalogue. Findings stopped recurring once the loop was closed.
- Letting the agent be its own beta tester. Giving it simulator control and asking it to drive every screen produced a better bug list than the human had time to write.
- Decisions recorded in one place. The project spec carries every product decision with its rationale. Later phases cite them by number.
Recommendations for the next small iOS app
- Front-load every Xcode step. List the project creation, targets, capabilities, cross-target memberships and binary assets before phase 1 and do them in one or two sittings. Each one is a pause in an otherwise autonomous build — here they cost 12h 40m of wall clock for about ten minutes of clicking, a quarter of the project's calendar span. Ordering the phases so every manual step falls in one window would have recovered most of it.
- Give every control an accessibility identifier from the first screen. Nothing without one can be driven by a UI test or an automation CLI, and the constructs that silently break XCUITest are worth reading about before you write a view.
- Give the agent the simulator early. The self-driven beta pass should have happened after Phase 6, not after Phase 9.
- Budget a device pass for the gate-unprovable set. CloudKit sync, Live Activity render, widget render and cross-process capture. Write the list down so a green gate is never misread as coverage.
- Write a pure core and lint the boundary. It is a one-line grep and it caught a violation fifteen times.
- Close the retro loop before the next phase. The value is not in writing the retro; it is in the next design reading it.
- Auto-format before you strict-check. Nearly half of this project's gate failures were lint violations, none of them defects. Run the formatters in --fix mode as a pre-gate step; a model cannot see a 101-character line without being told.
- Decide what an unattended run does when the network drops. The default is that it stops silently and waits for you. Nine hours went that way here.
- Don't ask a question you have already answered. The single longest pause in the project was the agent asking permission to do the thing it had just recommended. Let an autonomous run take its own recommendation and report the choice, reserving the interrupt for decisions that are genuinely the human's — here, two of seventeen were.
- Keep the transcripts if you will want a cost story later — raise cleanupPeriodDays and copy the directory somewhere durable. The 30-day cleanup ran before this write-up started, and only a backup saved it.
SEP 3, 2026
A Definition of AI-Native Software Engineering
So, after trying out a few different terms to capture the shift in software engineering caused by LLM coding models, I believe AI-Native Software Engineering...
SEP 1, 2026
"Luxurious" Software Development with AI
If you ask any even slightly jaded software developer, they've likely worked on a project where having "nice things" cost too much developer time according to...