Blog
Notes on Swift, Rust, and what I learn while building apps.
- Moving a test suite from XCTest to Swift Testing
Parameterised tests removed a third of my test code, and one behavioural difference caught me out.
- Read the source, not the documentation
The habit that changed my rate of improvement most, and how to start without being overwhelmed.
- What a KeyPath costs
They look like syntax and they are objects. When that matters, and when it does not.
- SwiftData after eight years of Core Data
What it genuinely fixes, what it hides, and the three places I still drop down to the layer underneath.
- UIViewRepresentable done properly
Three methods, one Coordinator, and the update loop that will freeze your app if you get it wrong.
- Calling Rust from Swift
A shared core in Rust with a Swift app on top: the FFI boundary, the memory rules, and whether it was worth it.
- Retry with backoff, and the requests you must not retry
Twenty lines of async/await, plus the decision that matters more than the algorithm.
- Making SwiftUI previews actually usable
Previews stopped working in my app, so I stopped using them. Getting them back changed how I build screens.
- Six App Store rejections and what actually fixed them
Every one was avoidable. What the guideline number really meant, and the reply that got each build approved.
- When unsafe is justified, and what you owe when you use it
unsafe is not a way out of the borrow checker. It is a promise, and here is what writing that promise down looks like.
- `lazy var` is not thread-safe, and `static let` is
Two Swift features that look like the same idea. One is safe from concurrent access and the other will corrupt memory.
- Using the Keychain directly, without a wrapper library
Four functions and about sixty lines. The API is unpleasant, and it is not big enough to justify a dependency.
- Rust iterators really do cost nothing
I did not believe the zero-cost claim, so I looked at the assembly. Here is what the compiler actually does.
- Why Swift won't let you write `string[0]`
It looks like hostility to beginners. It is the only correct answer, and the reason is one emoji.
- Why I write books instead of threads
Long-form is worse for reach and better for everything else. What writing a chapter does that a thread cannot.
- Making background tasks actually run
BGTaskScheduler is a request, not a promise. Six reasons mine never fired, and how to test one in ten seconds.
- `.task` and what actually cancels it
The modifier that replaced onAppear, the `id:` parameter that fixed my stale data, and the one case where it does not fire.
- SQL checked at compile time with sqlx
Your queries are verified against a real database while you build. What that catches, and what it costs your CI.
- The Codable conformances that break in production
Six ways a synthesised Codable fails against a real API, and what to write instead.
- Getting an app from 142MB to 38MB
Where the size actually was, and the one number Apple reports that is not the one users see.
- Replacing singletons with the environment
Twelve `.shared` properties made half the app untestable. What I replaced them with, and the two I kept.
- This site ships no JavaScript framework, on purpose
What a static Astro site with one shader and some vanilla JS costs to build and maintain, four months in.
- The shape of an axum service that stays maintainable
Extractors, state, middleware and error handling — the structure I arrived at after getting the first two wrong.
- My first Swift macro, and why I deleted the second one
Macros are the most powerful metaprogramming tool Swift has. Two of the three I wrote should have been functions.
- Symbolicating a crash report from the App Store
A wall of hex addresses becomes a line number in three commands, once you have the right dSYM.
- NavigationStack with a typed path
Programmatic navigation that survives a deep link, a state restore and a 'pop to root' — in about forty lines.
- A Rust CLI that feels finished
clap's derive API, plus the four things that separate a script from a tool people will use.
- Writing a result builder, and when not to
The mechanism behind @ViewBuilder, built from scratch for a query DSL — and the honest case against doing it.
- Four times I optimised the wrong thing
Every one of these was a confident guess, and every one was wrong. What the profiler actually said.
- Move your build settings into xcconfig files
Settings in the project file are invisible in review and a merge conflict waiting to happen. Text files fix both.
- dyn Trait or generics: the decision is about the collection
Static and dynamic dispatch in Rust, and the question that settles it faster than benchmarking does.
- Turning on strict concurrency in a four-year-old app
800 warnings on day one. The order that made it tractable, and the two changes that removed most of them.
- Splitting an app into local Swift packages
What modularisation actually bought, what it cost, and the dependency mistake that took a week to undo.
- PreferenceKey, explained by building one
Data flows down the SwiftUI view tree through the environment. PreferenceKey is how it flows back up.
- What I look for in a code review
Four things, in order. Style is not one of them, and neither is anything a linter can catch.
- Lifetimes in structs: when to borrow and when to own
The `'a` in a struct definition means something specific, and it is usually a sign you should have used String instead of &str.
- Task.cancel() does almost nothing
Cancellation in Swift concurrency is cooperative. If your code never checks, cancelling it changes nothing at all.
- Cutting a 4-minute Xcode build to 50 seconds
Measuring which functions are slow to type-check, and the three expression patterns that caused most of it.
- Why my List scrolled badly, and the four things that fixed it
A SwiftUI list that stuttered on an iPhone SE. None of the causes were what I assumed.
- Arc<Mutex<T>> and the three times you do not need it
The default answer to shared state in Rust, and why reaching for it first is usually a design shortcut.
- The actor reentrancy bug I shipped
Actors prevent data races. They do not prevent your invariants breaking across an await, and I found that out in production.
- What I would study first, if I were starting over self-taught
Four years in, the gaps I actually had were not the ones I worried about. What I would prioritise now.
- Finding a retain cycle with the Memory Graph Debugger
A leak that added 4MB per screen push. How to find one in five minutes instead of reading every closure in the file.
- Stop reaching for GeometryReader
It is greedy, it runs in the sizing pass, and four newer APIs cover most of what people use it for.
- Why I stopped agonising over the Rust async runtime
async-std, smol, or tokio. The decision took me a week and should have taken ten minutes.
- Making a type Sendable without reaching for @unchecked
@unchecked Sendable silences the compiler and keeps the bug. Four ways to earn the conformance properly.
- Cutting cold launch from 2.1s to 900ms
Where the time actually went in an app I was sure was already fast, and the measurement that made it visible.
- Migrating ObservableObject to @Observable, and the redraws that disappeared
A mechanical migration with one non-mechanical part, and a performance win I did not expect to be that large.
- A method for debugging, for when you have run out of ideas
Not tips. A procedure — the one I fall back on when the bug has beaten me for a day and guessing has stopped working.
- thiserror or anyhow: the line is the library boundary
Two crates that look like alternatives and are not. The rule that decides which one, in one sentence.
- weak, unowned, and the crash I shipped
The rule everyone quotes is about lifetimes. The rule that actually keeps you safe is about who outlives whom, and I learned it the expensive way.
- Reading a Time Profiler trace without guessing
Instruments shows you a flame graph and a million samples. The four settings that turn it into an answer.
- The animation that jumped, and the `if` that caused it
A SwiftUI transition that refused to animate, and the two-branch conditional that was quietly destroying the view every time.
- The borrow checker is a reader, not a guard
Coming from Swift, I fought Rust's borrow checker for a month. The model that made it stop being a fight.
- What `any` actually costs
Swift made existentials explicit for a reason. Here is what the box does, and when a generic is the better answer.
- Why this blog exists
A place for the notes that are too short to become a book.