A first-person devlog on the architecture, the pixel-art pipeline, the animation trickery, the "no backend" bet, and everything I learned shipping to the App Store alone.
Hey everyone. I want to share my first real experience building a mobile game for iOS — a roguelite. It was a pet project that, to my own surprise, grew into an App Store release, so I’ll cover both the idea and the technical bits. Hope it’s an interesting read.
I was taking an iOS development course and figured the best way to actually retain the material was to build a few real projects. One of them became LootShrine — link to the App Store.
It’s a roguelite focused on inventory management and turn-based combat: multiple modes (an endless mode and a "story" run across 100 floors), you climb a tower of 100 floors, collect loot, equip it, or sacrifice items at the shrine for permanent power that lasts the run. Every item is a choice: equip it, "absorb" it for a temporary battle buff, or sacrifice it. A shout-out to Megaloot, which inspired me — the idea for a game like this partly came from playing it, since I couldn’t find anything comparable on mobile.
Next — how it actually works under the hood.
Let me note one key constraint I set from the start: no custom backend — only Apple’s built-in services, because I didn’t want to over-complicate things (and it’s expensive right now). This was a conscious decision (for a solo project I don’t have the time or the interest to also stand up and maintain a server), and it shaped a lot of the architecture: saves, sync, competitive features — everything is wrapped into CloudKit and Game Center (more on this below).
The base stack is pure SwiftUI (iOS 17+/macOS 14+), no third-party UI frameworks. Only two external SPM dependencies: RevenueCat for in-app purchases and TelemetryDeck for analytics. I want to specifically call out TelemetryDeck — it’s a privacy-first solution: only anonymous aggregated statistics, nothing tied to the player’s identity (which makes App Store review easier, and makes it easier for players to install a game that isn’t asking for extra permissions). Everything else is Apple’s native stack.
Architecturally I went with a Redux-like approach rather than MVVM or a ready-made engine (though that variant would definitely have simplified some development), and here’s why it turned out to be a good fit for the game (or at least I think so):
Codable struct GameState
(with sub-states inside: RunState, PlayerState, BattleState, ShopState, MetaState, etc.).
For a game this matters: dozens of screens need to see a consistent state.enum GameAction that lists everything that can happen in the game:
playerAttack, purchaseItem, sacrificeItem, advanceFloor, endRun, and so on.(State, Action) -> State, with no side effects: no I/O, no network, no hidden random state inside.
Turn-based combat maps beautifully onto the model “state → action → new state”.@Observable. I use the native Observation from iOS 17 (@Observable)
rather than ObservableObject/Combine — less boilerplate and more precise view invalidation.store.dispatch(.action).
Separately about reducer orchestration: there’s a root gameReducer that receives an action
and delegates it to specialized sub-reducers — BattleReducers, ShopReducers, ShrineReducers,
RunReducers, EventReducers, MetaReducers, and others.
So the root reducer is a dispatcher, and all the domain logic is broken up across modules.
This decomposition helps a lot when combat logic grows: my combat reducer file is already several thousand lines,
and without splitting it, it would be hell.
Content (items, enemies, sets, zones) partially lives in JSON, partially in declarative Swift tables. Side effects (saves, sound, item generation, Game Center, cloud) are moved to a separate services layer.
All the pixel art — enemies, items, skill icons — is rendered in SwiftUI by slicing large texture atlases.
Meaning I don’t store thousands of separate PNGs (in some cases I do have individual PNG/JPG assets,
but atlases are the main approach); instead, I keep big atlas sheets and cut out the needed frame:
Image(uiImage:) with interpolation(.none) (so the pixel art stays crisp),
and offset/crop based on the sprite’s coordinates in the atlas.
That’s how monster sprites, item icon grids, and skill-tree icons are all done.
I’m not an artist or an art director, but let me say a bit about the “graphics pipeline” too. I generated sprites, icons, status effects, and art in ChatGPT, and then finished them by hand: centered them, cleaned them up, normalized them to a common size, and packed them into texture atlases in Aseprite (a pixel-art editor). Those big atlases I mentioned above are the result of hand-assembly in Aseprite. Generation gives you starting pieces fast, but without manual polish and careful atlas layout, it just doesn’t fly in-game.
Originally the game was fully SwiftUI, including hit effects, deaths, and so on — I drew them as SwiftUI overlays on top of combat. And a problem emerged: animation timing in SwiftUI is unreliable (or maybe I just don’t know how to prepare it), because it’s tied to the view lifecycle. An effect could cut off mid-play, play at the wrong time, or not play at all during a redraw.
I thought about it and decided to move all combat VFX into a transparent SKScene (SpriteKit)
overlaid on top of SwiftUI combat. SpriteKit gives reliable SKAction.sequence with predictable timing
that doesn’t depend on SwiftUI redraws.
Essentially, one scene replaced a pack of SwiftUI overlays (attack effects + death “puffs”).
But a bridge appeared between the two layers, and this is exactly the bottleneck I plan to refactor (because even at this level of graphics complexity, it burns quite a lot of resources):
GeometryReader) and pass them into SKScene
as positions where the effect should hit.update,
repositions the emitters onto the current coordinates of the “floating” sprites.It works — but essentially I have two render pipelines (SwiftUI + SpriteKit) living side by side and syncing via passing coordinates every frame. That’s the point for optimization: I want to move to a more performant, cohesive scheme so I’m not shuttling positions back and forth.
Each hit/skill effect is assembled from two layers:
SKAction.animate(with: textures, timePerFrame:).
Textures are cached in SKTexture ahead of time so they aren’t assembled on the fly.SKEmitterNode on top (sparks, splashes, DOT drips, etc.).Crits, by the way, are a separately layered animation on top of the base hit, and “kill” is a sequence: first the attack effect plays, and after it completes — the death effect. It’s all fire-and-forget: nodes remove themselves after playing.
Mobs aren’t static — they have an idle animation, a custom SwiftUI animation/ViewModifier:
So it’s all an honest SwiftUI animation layer on top of atlas sprites, no SpriteKit.
As I said at the start, the game has no custom backend — everything is built on Apple’s native stack.
Game Center (GameKit) handles the competitive side:
CloudKit handles everything to do with player data and social features (specifically CloudKit, not Game Center):
This approach gave me a free and reliable infrastructure without a server side — for a solo dev, that’s a huge savings of effort.
Since the game is already released and people have saves, and I’m still supporting and updating the game —
adding new content — any change to the state schema can break someone’s progress.
Codable turned out to be treacherous here (I broke my own saves locally a couple of times, though never in production):
if you add a new non-optional field to a state struct, the old-save decoder crashes with keyNotFound,
even if the property has a default.
Optional? with = nil,
and read via ?? <default>.
And of course during development I couldn’t manage without agents (sorry) — I use Claude from Anthropic. Plus ChatGPT for art generation (see above). AI is good at accelerating routine work and drafts, but the architecture, atlas assembly, and balance polish all still have to live in my head and be done by hand.
What started as a course pet project turned into a real App Store game: solo development, native stack, Redux architecture, a SwiftUI + SpriteKit hybrid, and the entire infrastructure on CloudKit + Game Center with no backend of my own. Along the way I got hands-on with a bunch of things — from the limits of SwiftUI in animations to schema evolution in production.
If you have questions, I’ll happily answer them. Thanks for reading!
// TRY IT ON YOUR PHONE
First 10 floors free on the App Store. If you’d like to see the systems this article is about in action, that’s the best place to start. Questions? Email me at support@lootshrine.com — goes straight to me.
APP STORE