Claude Code and iOS development

To date, I’ve used Claude Code primarily for generating web-based content: HTML, CSS, and JavaScript. Because of the preponderance of publicly-available data for all three types of content, I haven’t been surprised at its general level of competence. But iOS programming is another story. I recently built an iOS app relying heavily on Claude Code, and the experience has convinced me that its capabilities extend to Swift and SwiftUI fairly well.

There’s a lot less data to train LLMs on Swift and SwiftUI than many other languages and frameworks. Swift is just over ten years old, whereas languages like C (50+ years), C++ and Objective-C (40+ years), Ruby and Python (30+ years), and HTML/CSS/JavaScript (~30 years) have decades of history. SwiftUI is even newer—Apple released it in 2019. And unlike many popular app development frameworks like React Native, Rails, the Android SDK, Jetpack Compose, and Flutter, whose source code can be included in training data, SwiftUI (and its predecessor, UIKit) is proprietary.

Given these limitations, you might expect generative AI to struggle with coding for Apple platforms, especially when working with modern platform-native technologies. There’s a bit more nuance than that—for example, LLMs are able to transfer general coding knowledge from one language to another—but overall, it’s reasonable to assume that developing a modern iOS app using Claude Code would be a rougher experience than it is on other platforms.

I’m happy to report that, at least for smaller apps, this doesn’t appear to be the case.

The motivation

The best way to develop software is to identify a need that drives you and keeps you focused. Here’s how I found mine.

My 7-year-old son got the grownup version of Ticket to Ride for his birthday, it was an immediate hit. Since then, he, my wife, and I have played quite a number of games, with other family and friends joining in at times. It’s a strategy game with a relatively complex scoring scheme, requiring a fair amount of discipline to keep up as players compete to make rail connections between cities, all of which earn varying numbers of points. On top of that, multi-city routes count for extra points at the end of the game, and the player with the longest continuous route earns an additional bonus.

In most of the games we played, we wound up falling behind in scorekeeping. This generally resulted in an end-of-game routine where we had to go back and tally everything up again to make sure nothing was missed. Then we had to manually calculate the multi-city route points and inspect the board to determine which player had the longest route.

As I’m an iOS developer, this seemed like a ripe opportunity to create an app to automate score tallying. The structure and interface could be relatively simple due to the limited scope. Everything could happen right on the device, so I wouldn’t have to fuss with a web service.

The plan

I started by creating a planning file for the LLM to use as a reference—standard practice for Claude Code. Here are a few excerpts:

This is a universal macOS/iOS/iPadOS app to keep score in the board game Ticket to Ride. It is coded in Swift and SwiftUI. It’s a single-window app with a clean interface.

The game knows all the cities on the board, and the length of connections between them. It also knows each of the tickets: the beginning and end cities, and its score. It doesn’t track game board state, e.g. whose trains are where.

For entry of completed lines, the user taps a button associated with the player. The modal UI contains drop-down lists for origin and destination cities. The destination drop-down is disabled until the user selects the origin city, and then the destination drop-down will contain only adjacent cities. The user confirms their selection to add it to their score.

There is a button to initiate the end-of-game UI, which uses a modal to ask each player in turn for their completed and incomplete route tickets. Upon completion, the modal is dismissed and the final scores are displayed for all players.

I didn’t include much in the way of architectural detail or add any code snippets, so it was a pretty bare-bones starting point. I wanted to see how much it could do without a lot of hand-holding.

Based on my brief description, I had Claude Code put together a more detailed development plan. It did a credible job, outlining a bunch of the foundational code and breaking the project into phases.

The process

I made some adjustments to the original development plan, not to correct errors so much as to redirect towards my preferences and experience. Then I set it to work. It generated the game logic and data management, as well as some of the core screens, with no serious flaws. I corrected a few things on my own, and prompted for other adjustments.

I had an app prototype ready for play testing within 2 days. On my own, it would have taken me closer to a week, simply due to the volume of rote coding. Being able to specify a few key details and have an LLM translate them into code can be a huge efficiency boost, if you know how to manage it properly. I’m gaining proficiency; my planning and prompting generally result in pretty decent code. It still requires thorough review and tweaking afterwards, which of course would happen concurrently if I coded it myself.

The prototype

I uncovered plenty of little issues in my play testing, mostly with the data that models the game board. I was sure that Claude could generate the data—it’s a perfect use case for an LLM. But I was also unsurprised to discover plenty of omissions and errors. None of them were all that hard to solve, but I did have to do it manually.

I handled the issues in the code both through prompting and by hand. There were plenty of smaller things that took little effort to fix.

Adding in the manual work from this process, I still estimate that I saved myself a lot of time overall.

The refactor

The only major issue I found in the prototype was some overcomplicated code driving one of the screens. LLMs have a tendency to do this kind of thing if left to their own devices; if I had done more work upfront to specify some code patterns, I might have saved myself some time here. As it stood, I had to undertake a sizable refactoring effort.

For those of you who are non-developer readers, refactoring code involves changing how it works and how it’s structured, while keeping its function and output the same. In my case, the SwiftUI code for the end-of-game interface was poorly designed, with twisted data management and overlapping responsibilities, and a structure that was hard to reason about and maintain.

To handle the work, I discussed the problems in detail with Claude, and had it generate the refactoring plan. It didn’t always pick the most logical solution to a given problem, so I made revisions to make sure that I wouldn’t be digging myself another hole.

The work went pretty smoothly; I followed my typical pattern of prompting for most of the code, and then reviewing and making tweaks. I was pretty satisfied with the resulting cleaner architecture.

This work required quite a bit more time and diminished the ultimate efficiency benefits. That said, I don’t think I came out in the red, and the process was enjoyable on the whole. And I think I probably saved myself some development time—just not a large multiple like 5x.

The artisanal stuff

Coding by hand is fun, and I don’t ever want to give it up entirely. It’s a way for me to get into a flow state, and prompting Claude Code will never compare to that experience. So when I got to a point when I wanted to make some interface tweaks, and I could see the shape of the solution, I decided I’d tackle it myself.

After spending lots of time in “senior dev” mode, directing and managing a junior developer, it was a refreshing change to dive into the code and play. I spent several hours in that happy place: writing code, testing, debugging, consulting references and documentation. The problem turned out to be more complicated to solve than I had anticipated—which, for an experienced software developer, is paradoxically entirely unsurprising—so it was deeply engaging, and, luckily, not so intractable as to become frustrating.

I made other hand tweaks to the code as well. I’m sure that I could have used Claude for at least some of this work, but I’m less certain that it would have gotten the project to where I needed it. There’s a certain level of polish that, at least on Apple platforms, may still be lacking with coding agents.

Of course, my assessment is the very definition of anecdotal evidence. I’m not disciplined (or motivated) enough to keep detailed data to back up my claims, but I’ll definitely monitor my impressions as I continue to work with LLMs on future iOS projects. I’m confident that things haven’t plateaued.

The app

TicketBook is currently in a TestFlight beta. If you’re a fan of the game and have an iOS device, check it out!

Written entirely without the assistance of AI unless otherwise mentioned, with the exception of extremely light proofreading for grammar and spelling.