I reviewed my own iOS app with five parallel agents: here's what they found
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 had a few thousand lines of a SwiftUI app sitting on main with no second pair of eyes on any of it. Solo project, so there’s no teammate to send a PR to. I wanted a real review - not “looks good,” but someone actually hunting for the bug I can’t see because I wrote it. So I split the codebase into five slices and put a separate agent on each, in parallel, each with a focused brief.
The result was more useful than I expected, and not for the reason I expected. Here’s how it went and what it actually caught.
Why five, and why split by area
One reviewer over 3,600 lines gets shallow fast - by file forty it’s pattern-matching, not reading. Five reviewers over ~700 lines each can actually read every line in their slice and hold it in their head. So I cut the app along seams that already existed: the MusicKit service layer, the StoreKit and persistence layer plus the data models, the Discover and Playlist features, the History and Settings and app-shell code, and a fifth one on tests and CI config.
Each got the same kind of brief: here’s the stack and the conventions, here are your files, look for correctness bugs, Swift 6 concurrency problems, SwiftUI observation mistakes, and convention violations - and only report real issues, cite the line, and tell me what’s genuinely well done too. That last part matters. A reviewer told to “find problems” will manufacture them. A reviewer told “find real problems and also tell me what’s solid” stays honest.
The thing I almost skipped: verifying the findings
Here’s the part I want to be honest about. The agents came back with a clean, confident list. The tempting move is to take it at face value and start fixing. I didn’t - I re-read the actual source for every high-priority finding before believing it. That caught the difference between a finding that’s real and one that’s plausible.
Two findings survived that check and turned out to be genuine bugs:
The first was in my cross-source matching key. The app matches the same album across three places that don’t share IDs - your library, the Apple Music catalog, and your dismissed list - by normalizing title and artist into a single string. The doc comment promised the key was whitespace-insensitive. The code trimmed the title and forgot the artist:
// before
(title.trimmingCharacters(in: .whitespacesAndNewlines) + separator + artist)
.lowercased()
So an album dismissed with an artist string that had a trailing space would quietly stop matching, and the release I’d told the app to hide would come back. Low odds in practice, one-line fix, but it’s exactly the kind of asymmetry you skim right past in your own code because you know what you meant.
The second was a race in the History tab. Pull-to-refresh resets the list to page zero; “load more” appends the next page using the current count as its offset. Both are async. If a load-more is in flight when a refresh lands, the load-more captured its offset before the refresh reset everything, so it appends a stale page on top of the fresh one - duplicate or missing rows on a feed that’s already shifting under you. The guard I had prevented two load-mores at once, but never considered refresh racing load-more.
The most valuable finding wasn’t a bug
The single best catch was about my tests, not my code. The reviewer pointed out that my mock service has no way to throw - every method returns sample data, none can fail. Which means every catch branch in my view models, every “show the error state” path, is completely untested. The failed-to-load UI state is a core part of the app and it had zero coverage. A regression that mangled error handling would sail through CI green.
That’s the finding I’d never have written down myself, because the tests were all passing and passing tests feel like done. They were passing because they only ever exercised the happy path. Green is not the same as covered.
What I fixed now, and what I wrote down for later
I fixed the cheap, safe things in one pass: the artist-trimming bug (with a test that pins the behavior the comment promised), and two pieces of dead scaffolding - a placeholder data model still registered in the SwiftData container that would have leaked into my CloudKit schema later, and an empty placeholder file whose own header said to delete it once a real utility showed up. Three real utilities had shown up. The race and the error-path test coverage are bigger jobs, so they’re written down as findings to tackle deliberately rather than rushed in alongside cleanup.
The honest takeaway: the parallel split made the review thorough, but the verification step is what made it trustworthy. Agents are good at generating a plausible list fast. The judgment about which items are real, which matter, and which to fix now versus log for later - that part stayed mine. That feels like the right division of labor.
Related reading
Letting Claude Code drive Xcode: the synchronized-groups trick
An agent can't click the New Project wizard. Xcode 16's file-system synchronized groups are the seam that lets it own every source file anyway.
Setting up Claude Code for an iOS project
Writing the CLAUDE.md for a new iOS app before a line of Swift exists - the MusicKit-in-simulator trap, CloudKit's SwiftData rules, and why the learning-project framing came first.
I only fixed the screenshot I was asked about, not the ones that were also broken
One recaptured marketing shot looked done - until the deflating question: is this really every screenshot? The other three in the same set were stale too, each in a different way.