Notes from the field.
Mostly bruises.
Field Notes are the iOS development write-ups you only get from shipping the thing. No docs-tier explainers, no pretending the rough edges aren't there, no "here's how it should work" without the part where it didn't.
Think of it as the conversation you'd have with another iOS dev at a conference, after the talks, when everyone's tired enough to be honest. Concurrency migrations that bit us. SwiftData edges we found in TestFlight at 2 AM. Xcode 26 features that look great in the keynote and weird in your project. The opinions you only earn by deploying.
What this isn't
- — A SwiftUI tutorial. Apple has 800.
- — A "10 Tips for iOS Developers" listicle.
- — Beginner-friendly. Mostly.
- — News. The blog handles news.
What this is
- — Honest reports from shipped apps.
- — Specific bugs, specific fixes, specific tradeoffs.
- — Opinionated. Sometimes wrong. Usually useful.
- — What we wish someone had told us six months ago.
App Intents + Spotlight Semantic Search — How I Doubled BetFree's Spotlight Opens in Two Weeks (and the Tests That Caught Three Bugs Before Shipping)
Post-series field note. iOS 26's Spotlight semantic index quietly became one of the highest-intent discovery surfaces on the phone — and most indie apps still treat it as an afterthought. This is the TDD-first walkthrough of indexing App Intents and CSSearchableItems on BetFree, the four bugs I caught with tests before they hit the App Store, and the WWDC 2026 surface area worth front-loading before June 8.
Build-Time Optimization for a Solo Developer with Five Apps — The Numbers, the Setup, and the Habits That Cut My Cold Build From 142s to 38s
Day 30 of 30. The series finale. One developer, five shipping apps, one Mac Studio. This is the honest tour of my build-time setup in 2026: module isolation across SPM, prebuilt binary frameworks, derived-data hygiene, the Xcode 26 build-system improvements that actually moved the needle, and the four habits I had to break to stop shipping a slow build. Concrete numbers before and after, on real apps — Invoize, Renovise, ThinkBud, BetFree, and Reset — not synthetic benchmarks.
Custom Swift Macros in 2026 — When They Are Actually Worth the Trouble (and a #URL Macro That Validates Links at Compile Time)
Day 29 of 30. Swift macros are the most over-recommended feature of the last three years. This post is the honest one: what @freestanding vs @attached actually means in practice, the four-question gate I run before writing one, the over-engineering trap that cost me a week on Renovise, and a real #URL(_:) macro that turns URL(string:)! into a compile-time error instead of a runtime crash — with TDD-first tests that pin the behavior, not the implementation.
UIKit Tricks SwiftUI Still Can't Do in 2026 — Five Places I Have Honestly Reached for UIViewRepresentable (and the BetFree Screen Where I Gave Up)
Day 28 of 30. SwiftUI in iOS 27 is finally adult enough to ship most screens with — but not all of them. This is a 1700-word, TDD-first walk through the five places I have actually dropped to UIKit in production this year: a custom number-pad keyboard, a two-finger pinch-and-rotate gesture, a 10,000-row infinite scroll, a PDF receipt renderer for Renovise, and a BetFree onboarding pager where I finally surrendered after two days of fighting TabView. Decision matrix, the UIViewRepresentable wrappers, and the verb-level tests that pin the behavior so swapping back to SwiftUI later costs zero.
Combine in 2026 — Three Pipelines async/await Still Can't Touch, Built TDD-First in a Real Gambling-Blocker App
Day 27 of 30. Combine is not dead. It just stopped being the default. This post is a 1500-word answer to the question 'should I delete the Combine code I wrote in 2020?' — with three concrete production pipelines (search-bar debounce, multiple subscribers to one upstream, complex multi-stream merging) where async/await is the wrong tool, the side-by-side rewrite that proves it, and the TDD test that pins the behavior on the verb instead of the framework.
Live Activities and the Dynamic Island — A Decision Matrix, a 200-Line Implementation, and the One Question That Should Kill 80% of Them Before You Open Xcode
Day 26 of 30. ActivityKit in production, BetFree as the example, TDD-first. The decision matrix I use to choose between a widget, a Live Activity, and a notification — plus a real, shippable Live Activity for a session over an hour, with the lock-screen layout, the Dynamic Island compact/expanded/minimal regions, push-token plumbing, and the three places ActivityKit silently does nothing in production.
Interactive Widgets in 200 Lines — A One-Tap BetFree Reset Right on the Home Screen, Built TDD-First
Day 25 of 30. A real, shippable interactive widget — a streak counter for BetFree with a 'reset' button that fires an App Intent, updates SwiftData, refreshes the timeline, and writes back to the app — built from an empty target with tests in front of the code. Includes the simulator screenshot, the App Group plumbing, the three gotchas every tutorial skips, and the decision matrix for when an interactive widget is worth shipping at all.
SOLID in Swift Without the Theory Dump — A Real Renovise Invoice Form, Refactored One Letter at a Time
Day 24 of 30. SOLID, but not the version where someone explains the Liskov Substitution Principle with shapes and birds. One real screen from Renovise — an invoice form that violated four of the five letters in 320 lines — refactored letter by letter, each step justified by a concrete bug, a concrete test, or a concrete future change the old shape would have made painful.
Retry, Token Refresh, and Request Deduplication in a Custom Swift Networking Layer — The Decorator Chain That Survives Production
Day 23 of 30. The production-grade additions that ship on top of yesterday's 100-line networking spine: a retry decorator with exponential backoff and jitter, an actor-based auth interceptor that refreshes tokens exactly once across concurrent callers, and a dedup decorator that collapses identical in-flight requests into one round-trip. All three are decorators on Day 22's HTTPClient, all three are test-driven, and none of them touch the original spine.
A Custom iOS Networking Layer in 100 Lines: URLSession, async/await, a Generic Endpoint, and Typed Errors — No Alamofire, No Moya, No Apologies
Day 22 of 30. The minimal networking layer I actually ship with — one protocol-shaped client, one generic Endpoint, one typed error tree — built test-first against a fake URLProtocol so the suite never touches the network. Why third-party clients are usually a tax you do not need to pay, what 'one hundred lines' really buys you, and the seams that make Part 2 (retry, token refresh, deduplication) drop in without rewrites.
TDD for SwiftUI Without the Academic Ceremony: Don't Test the View, Test the Observable Model Behind It
Day 21 of 30. The honest red-green-refactor cycle for a real Renovise feature — invoice line-item editor — written test-first against an @Observable view-model. Why testing SwiftUI views is mostly a category error, the four signals that tell you you're testing the wrong thing, and the workflow I actually run on a Tuesday afternoon. Sits directly on top of yesterday's Swift Testing migration.
Swift Testing in Anger: Migrating from XCTest, Side-by-Side on a Real File, and the Two Traits I Now Treat as Defaults
Day 20 of 30. Why Swift Testing changed the way I write the layer of the app *under* the tests, not just the tests themselves. A real XCTest file converted to @Test / #expect with parametrized arguments and the .serialized + .timeLimit traits — plus the four migration gotchas (setUp lifetimes, async, expectations, MainActor) that bit me in production.
Dependency Injection in Swift Without a Framework: Protocols, @Environment, and Why Resolver Is Usually Overkill
Day 19 of 30. The honest case for skipping Resolver and Swinject in a SwiftUI app. The four DI shapes you actually need — constructor, @Environment, factory closures, and a typed Composition root — with a TDD-style parametrized test suite that locks the seams down. Slots straight into the four-package SPM shape from Day 18.
Modular Architecture with Swift Package Manager in 2026: When to Split, When Not To
Day 18 of 30. The counter-argument to the modularization hype. The honest build-time math behind splitting an iOS app into Swift packages, the threshold below which it costs more than it saves, and the four-layer SPM shape I actually use in production — with TDD-style contract tests that survive every refactor.
Server-Side Receipt Validation in 2026: The App Store Server API Without a Backend Team
Day 17 of 30. The third and final part of the StoreKit 2 series — why client-side verification is not enough, the App Store Server API JWS flow, and the smallest Cloudflare Workers shape that gives a solo indie real server-side validation without standing up a backend team.
StoreKit 2 — Free Trials, Intro Offers, Win-Back, and Promo Codes (The Money Indies Leave on the Table)
Day 16 of 30. The four StoreKit 2 offer types every indie app should ship — App Store Connect configuration plus the Swift code that handles eligibility, redemption, and the win-back path most apps never wire up. Part 2 of a 3-post StoreKit 2 series.
StoreKit 2 Paywall from Scratch — the Smallest Working Subscription, No RevenueCat, No SDK, No Apology
Day 15 of 30. The minimal working StoreKit 2 paywall — products loaded from a .storekit file, purchase flow with the transaction listener nobody shows you, restore flow, and the Swift Testing harness that pins the entitlement contract before the first refund hits. Part 1 of a 3-post series.
SwiftData in Production in 2026 — What's Finally Ready, What Still Bites, and the CloudKit Gotcha That Killed My TestFlight Beta
Day 14 of 30. Three years in, SwiftData is the default — except for the four shapes where it isn't. A field report from two shipping iOS apps: the migration headache that made me rebuild a whole feature, the CloudKit sync bug I found at midnight, and the Swift Testing harness I now run on every model change.
@Observable vs ObservableObject in Instruments — A 1,000-Item List, Frame Times, and the One Place Where the Old Way Still Wins
Day 13 of 30. I instrumented the same 1,000-row SwiftUI list with both @Observable and ObservableObject, recorded the frame times, and the property-level tracking story is real — but there's one shape where it costs you. Numbers, traces, and the test harness that pins it all down.
The SwiftUI Property Wrapper Mental Map: @State, @Binding, @Bindable, @Environment, @Observable in One Page
Day 12 of 30. After three teams, three whiteboards, and one beta tester who finally said 'I don't think any of you actually agree on what @Bindable does,' here's the decision table that resolves 90% of property-wrapper confusion in SwiftUI 2026 — with the five mistakes I keep flagging in code review and a Swift Testing harness that pins each rule down.
@State Doesn't Have @StateObject's Free Lunch — The Init-Cost Gotcha I Keep Seeing in Code Reviews
Day 11 of 30. @StateObject took an @autoclosure. @State doesn't. Migrating one to the other looks like a one-liner and silently makes your view re-run an expensive init on every parent update. Here's the recording, the measurement, and the three ways to fix it.
Stop Parsing Strings Out of LLMs: The Three @Generable Patterns I Actually Ship
Day 10 of 30. What @Generable does to the prompt under the hood, how Apple's structured output compares to OpenAI and Anthropic tool calling, and the three patterns that survived contact with a real app — including the one that quietly fixes 'the model returned an empty array'.
Foundation Models in a Real App: ThinkBud's First On-Device AI Feature, And the Night I Cooked My Battery
Day 9 of 30. Apple's Foundation Models framework gives every iOS 26 app a 3-billion parameter LLM for free, on-device, no backend. Wiring it into ThinkBud's auto-card generator took an afternoon. Making it not melt the iPhone took the rest of the week.
Killing the Last ObservableObject in ThinkBud — What @Observable Actually Breaks
Day 8 of 30. Spent Friday converting ThinkBud's final twelve ObservableObject classes to the @Observable macro. Three things broke silently, two broke loudly, and one of them I only caught because a beta tester had bad Wi-Fi.
TipKit Got Liquid Glass and Quietly Broke Half My Tooltips
Day 7 of 30. iOS 26 swapped TipKit's default tooltip rendering to Liquid Glass material — which is great, until your custom Tip styles silently stop adapting. Walking through the migration on ThinkBud's onboarding, with the gotcha that cost me an hour.
Icon Composer and the New App-Icon Era: One SVG, Four Variants, Zero Photoshop
Day 6 of 30. Apple's Icon Composer turns a single source asset into the full light, dark, clear, and tinted set iOS 26 demands. Walking through the actual workflow on an Invoize rebrand — and the gotchas that don't make it into the WWDC slides.
Custom Liquid Glass Components in SwiftUI: When (and How) to Leave the System Defaults
Day 5 of 30. Going past glassEffect() into custom shapes and layered glass — the three mistakes that quietly tank scroll performance, and a TDD walk-through of a custom Liquid Glass FAB cluster.
Liquid Glass for Existing SwiftUI Apps: What You Get Free vs. What's Worth Customizing
Day 4 of 30. The minimum-effort path to making an existing SwiftUI app look native on iOS 26 — what a recompile gives you, where glassEffect() is worth a custom touch, and the toolbar/backgroundExtensionEffect pieces nobody mentions.
Migrating a Real Project to Swift 6.2 Strict Concurrency: 86 Errors → 3 Annotations
Part 3 of 3: a real before/after of an Invoize strict concurrency migration. Actual compiler errors, the one genuine race condition hiding under three false positives, and the fixes — test-first, with the diff that mattered.
@concurrent, nonisolated, and @MainActor: The Swift 6.2 Decision Tree
Part 2 of 3: four real iOS scenarios — UI update, network call, image decode, file I/O — and the exact concurrency modifier each one needs in Swift 6.2.
MainActor by Default in Swift 6.2: Apple Flipped the Script on Concurrency
Xcode 26 ships Swift 6.2 with a new default isolation model that makes @MainActor implicit for app types. Here's why Apple made the call, what it means for your existing code, and a test-first walkthrough of the before/after.
Want the structured version?
Field Notes are scattered insights from shipping. For the same content organized into a path from zero to App Store, hit the courses.
Browse Free Courses