How I brought a ten-year-old SpriteKit game back to the App Store: 588 compiler errors, a lot of deleted code, and what AI was actually good for.
In 2016 I shipped a small arcade game for iPhone and iPad called Coocader. It had some ads, never really took off, and I moved on to other things. The code sat untouched for almost ten years.
This autumn I decided to bring it back. Not as a remake, but as the same game, modernized properly. A few weeks later, version 1.1 is live on the App Store. This is the story of how that went, what broke, and what working with AI as a pair programmer was actually like.
The game in one paragraph
Coocader is inspired by Space Invaders, with one twist: blocks with silly little faces move down toward your ship, and you can only destroy a block with a shot of the matching color. So you're constantly switching colors while aiming. Some blocks have shields, some shoot back, some drop gifts that help you… or ruin your run. If a block reaches the bottom, it's game over.
The starting point: Swift 2, Xcode 7, and 588 errors
The project was written in Swift 2 with SpriteKit. Modern Xcode doesn't even try to open that. Converting the old project file wasn't an option, so I started with a fresh project and moved the old sources in.
The first build produced 588 compiler errors.
Step 1: Cut dependencies before fixing code
Before touching the errors, I removed or replaced everything that wasn't my own code:
- A third-party Game Center wrapper (56 KB of source) became about 120 lines of plain GameKit.
- A custom alert library became
UIAlertController. - The ad integration was first hidden behind a small protocol, and later removed completely.
Every dependency I removed was one less thing to migrate. That alone took a noticeable chunk out of the error count.
Step 2: Compiler-driven, bottom-up
Instead of jumping around, I fixed the leaf types first, the small value types and helpers that everything else depends on, and worked upwards from there. Each fixed layer made the errors above it more meaningful. 588 became 0 in roughly two weeks.
I originally planned an intermediate step on Swift 5. In the end I went straight to Swift 6. Doing the work once was simpler than doing it twice.
The Swift 2 gotchas that actually hurt
Most errors were mechanical renames. A few were genuinely interesting:
- Overloads that Swift 2 silently resolved. An event listener class had four
init(callback:)overloads, and Swift 2 happily converted between them implicitly. Modern Swift refuses, so the overload structure itself had to change. - Method names colliding with SDK selectors. Two of my methods suddenly clashed with Objective-C selectors from the SDK and had to be renamed.
-
nil > 0was legal. Swift 2 let you compare an optional with a number. Some gameplay logic relied on that, which meant a block could become a "gunner" with an interval of 0. Fixing the compiler error changed game behavior, and I had to decide consciously which behavior was correct. - Escaping closures. Every closure that outlives its call now needs
@escaping, and finding out which ones do is a nice audit of your own architecture.
Delete instead of port
One of the most effective tools was simply deleting code. An old debug menu exported levels as JSON using AnyObject dictionaries. It blocked the migration for a long time. In the end I didn't fix it. I removed it while rebuilding the debug menu, and nothing was lost.
Things that look like bugs but aren't
Some of the most valuable notes I wrote during the project are about code that looks wrong but must stay that way. Two examples:
- A Game Center leaderboard ID is mapped explicitly instead of being derived from an enum name. Deriving it would produce an ID that doesn't exist, and scores would silently disappear.
- The scene uses a fixed logical size with
.aspectFill. Switching to a resizing scale mode would change distances, touch coordinates and rendering all at once.
Writing these down was important, because they're exactly the kind of thing a helpful refactoring, human or AI, would "fix".
More than a migration
Once the game compiled, I used the momentum:
- Levels moved from a custom file format to plain JSON, and block positions became integers.
- Purchases moved to StoreKit 2, with three one-time bundles instead of the old per-pack model.
- All ads are gone. 12 of 28 level packs (225 of 685 levels) and the endless mode are completely free.
- New color selector modes, a settings screen, and a loading screen with real progress.
Around the game I also built a companion website with Next.js in eight languages, a small level-pack API so new levels can be delivered without an app update, and a browser-based level editor. Everything is self-hosted with Docker.
What working with AI was actually like
I used AI coding assistants throughout the whole project, for the migration as well as for the website, the API and the editor. My honest summary:
- It's fast at the mechanical work. Hundreds of renames, API updates and boilerplate changes went by quickly.
- It's a great sparring partner. Talking through a migration plan, a tricky overload problem or a data model with something that answers immediately is genuinely helpful.
- It does not replace judgment. The
nil > 0case is a good example: the "correct" fix compiled fine and changed the game. You still need to know what you want, review everything, and make the architectural calls yourself. - Write things down. A living migration plan with decisions, traps and open points made every session better, whether the next session was mine or the AI's.
The biggest effect wasn't speed in any single task. It turned a project I had been postponing for years from "someday" into "now".
The result
After testing on simulators and a real iPhone and an eleven-day wait in the review queue, Coocader 1.1 went live on November 11, 2026. Same game, same silly faces, ten years younger under the hood.
If you'd like to try it or have a look at the project:
- App Store: apps.apple.com/app/coocader/id1080981061
- Website: coocader.com
Old code isn't a reason to throw a project away. Sometimes it just needs a plan, some persistence, and a good sparring partner.
Originally published on Medium.
