Setting up Claude Code for an iOS project
This app was later renamed Deep Cut Atlas. It’s called “Discoverer” throughout below, because that’s what it was called on the day this happened.
I’ve been wanting to build an iOS app for a while. The idea is called Discoverer: it surfaces new releases from artists in my Apple Music library that I haven’t added yet, so I don’t fall behind on music I actually care about. Simple premise, but MusicKit + SwiftData + CloudKit + StoreKit is a real stack to wire together.
I started by creating the git repo and writing a CLAUDE.md. On my homelab and blog projects, CLAUDE.md has become the most important file in the repo - it’s where I set the rules for how agents working in the codebase should communicate with me. For those projects the rules are mostly about infrastructure: don’t run destructive commands without asking, always check state files before making assumptions, that kind of thing.
For an iOS project the concerns are different. iOS has some sharp edges that a general-purpose coding agent will step on without a map.
MusicKit doesn’t work in the simulator. Ever. Library access, catalog search, recently played, playlist writes - all require a physical iPhone with an active Apple Music subscription. If I’m going to let an agent write code against MusicKit, it needs to know this from the start, or I’ll end up with code that can’t be tested without me realizing why. The standard approach is to define a protocol (MusicLibraryServiceProtocol) with both a real and a mock implementation, injected via environment so simulator builds use the mock automatically.
CloudKit changes the rules for SwiftData. Local-only SwiftData lets you have non-optional properties with no defaults. CloudKit does not; it crashes on schema initialization. Every @Model property needs to be optional or have a default. This is the kind of thing you find out the hard way on your first CloudKit project.
StoreKit sandbox config only loads through the Xcode IDE. Running xcodebuild test from the terminal doesn’t load it. All StoreKit purchase testing has to go through Xcode, then Run. This matters because I rely on xcodebuild for headless builds via xcbeautify, but any StoreKit work needs a full Xcode session.
@Observable not ObservableObject. Swift 6 + SwiftUI landed @Observable as the right pattern. Agents trained before this pattern stabilized will reach for ObservableObject by default. An explicit rule prevents the drift.
These went into the CLAUDE.md as “Critical constraints”, not as architecture comments but as hard rules agents must follow on every task. The learning-project rule came first, though: agents must explain each command before running it, interpret output after, and not assume I’m familiar with MusicKit or StoreKit internals. The homelab is a learning project. So is this.
The actual repo setup took about twenty minutes: git init, pulled the GitHub Swift .gitignore template (and added .DS_Store and DerivedData/, which the template omits), wrote a README with a project overview and placeholder setup steps, and committed all three files as the initial commit. Then gh repo create discoverer --private --push to create the GitHub repo and push in one step.
The repo is private until the app is ready for the App Store. At that point it becomes part of the release story. For now it’s just a place where the code will live and the rules are already written.
Next up: the actual Xcode project - a 3-tab TabView, MusicKit entitlement, StoreKit config file, and the folder structure from the CLAUDE.md. The CLAUDE.md is already there waiting for it.
Related reading
Building blog-manager, part 1: the Blog model and a UI that doesn't look like a scaffold
Starting blog-manager: a Rails Blog model with Active Record Encryption for per-blog credentials, and a CRUD UI I'd actually enjoy using.
Building Greenhouse: the rules engine that came before the app existed
Writing a full Rust rules engine and 22 tests before Greenhouse had a UI, or even a working Rust toolchain on the machine building it - and what broke the moment it actually compiled.
A CI failure playbook for a one-person Rails project
Writing down the rules for what to do when CI goes red on a solo Rails project, and the GitHub limitation that turned the merge gate into a load-bearing comment.