Field Notes

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.

31 notes so far New ones drop when something interesting breaks

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.
An over-the-shoulder shot of a person typing on an iPhone with the Spotlight search field expanded — the surface most indie iOS apps still under-index for, and the one Apple has been quietly upgrading every release since iOS 17.
Intermediate iOS 26 App Intents

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.

M
Mario
Close-up of a printed circuit board with traces fanning out from a dense central package. The metaphor for this post: a build pipeline is a board — most of the time is spent on a few hot traces, and the only way to make it faster is to find them, isolate them, and stop running current through everything else at the same time.
Intermediate Xcode 26 Build Times

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.

M
Mario
Close-up of a printed circuit board with intricate copper traces, surface-mount components, and a single LED catching the light. The metaphor for this post: macros are compile-time circuitry — invisible at runtime, but if you etch them wrong, the whole board fails before the user ever turns the device on.
Intermediate Swift Macros Swift 6.2

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.

M
Mario
A close-up of a green printed circuit board, copper traces and surface-mount components catching a single point of light. The metaphor for this post: SwiftUI is the case, UIKit is the board underneath, and once a year you still have to take the case off to fix something the case cannot reach.
Intermediate SwiftUI UIKit

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.

M
Mario
An overhead shot of pipes converging into a single junction on a concrete wall. The metaphor for what Combine still does best: many streams of values arriving at different rates, merged into one, with backpressure and timing handled by the plumbing — not by you. async/await is a great hammer; this is a post about the screws.
Intermediate Combine async/await

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.

M
Mario
A close-up of a stopwatch on a wooden surface — the metaphor for what a Live Activity actually is: an unobtrusive piece of *time you are spending on a thing*, persistent at the edge of your attention until the thing is over. Live Activities only exist to make a current activity glanceable; if your data has no clock attached to it, you almost certainly do not need one.
Intermediate Live Activities Dynamic Island

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.

M
Mario
A flat-lay of two iPhones — a black one and a silver one — on a pale blue surface next to a notebook. The visual stand-in for the home screen surface: the patch of glass your widget actually lives on, all day, every day, between app launches. Photo by Daniel Romero on Unsplash.
Intermediate WidgetKit App Intents

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.

M
Mario
A neatly organized workbench with hand tools arranged by purpose on a pegboard — the visual analogue of small, single-purpose types replacing one giant class that did everything. Photo by Barn Images on Unsplash.
Intermediate SOLID Single Responsibility

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.

M
Mario
An overhead shot of a busy industrial conveyor belt with packages being routed through three sorting stations — the visual analogue of decorators on a single HTTPClient, each station doing one job (retry, auth refresh, dedup) and passing the request through. Photo by Bernd Dittrich on Unsplash.
Advanced URLSession async/await

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.

M
Mario
A close-up of the back of a network patch panel with hundreds of Cat-6 cables routed cleanly into a switch — the visual analogue of a small, typed, predictable networking layer that does one thing per cable. Photo by Thomas Jensen on Unsplash.
Advanced URLSession async/await

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.

M
Mario
An overhead shot of a workshop bench with a marked-up wooden plank, a pencil, a square, and a hand-saw — the analog version of the red-green-refactor cycle: you mark the line first, you cut to the line, then you sand the rough edge. Photo by Greyson Joralemon on Unsplash.
Advanced TDD SwiftUI

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.

M
Mario
A row of glass beakers and laboratory measuring instruments backlit on a white surface — the analog version of a parametrized test suite: every column gets the same procedure, you see every measurement at once, and a single bad reading is obvious without rerunning the whole rack. Photo by Louis Reed on Unsplash.
Advanced Swift Testing XCTest

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.

M
Mario
A close-up of a patch panel with neatly bundled and labeled ethernet cables plugged into ports — the physical-world analogue of explicit dependency wiring: you can see every connection, every plug is the same shape, and pulling one cable does not bring down the rack. Photo by Massimo Botturi on Unsplash.
Advanced Dependency Injection SwiftUI

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.

M
Mario
A yard of intermodal shipping containers stacked into a colorful grid — the physical-world analogue of a well-factored Swift package graph: standard interfaces, swappable contents, and a crane that only has to lift one box at a time. Photo by Frank Eiffert on Unsplash.
Advanced Swift Package Manager Modular Architecture

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.

M
Mario
A close-up of a printed paper receipt curling on a wooden table — the analog version of the JWS-signed transaction your server actually trusts. Photo by Michael Walter on Unsplash.
Advanced StoreKit 2 App Store Server API

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.

M
Mario
A booklet of paper tickets fanned out — a visual stand-in for the four kinds of offers StoreKit 2 lets you hand to a prospective subscriber. Photo by Erik Mclean on Unsplash.
Intermediate StoreKit 2 Subscriptions

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.

M
Mario
A fan of credit cards on a flat surface — visual stand-in for the part of indie iOS where you stop building the app and start asking it for money. Photo by rupixen on Unsplash.
Intermediate StoreKit 2 Subscriptions

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.

M
Mario
A person at a laptop late at night, lit by the screen — visual stand-in for the midnight CloudKit sync debugging session that prompted this post. Photo by Tim Gouw on Unsplash.
Intermediate SwiftData Core Data

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.

M
Mario
A mechanical stopwatch on a wooden surface — visual stand-in for what this post is about: the actual milliseconds you save (or lose) when you migrate to property-level observation. Photo on Unsplash.
Intermediate SwiftUI Observation

@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.

M
Mario
A folded paper subway map opened on a wooden table — visual stand-in for what we're after today: one diagram that turns five property wrappers into a single readable network. Photo by Hugo Sousa on Unsplash.
Intermediate SwiftUI Observation

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.

M
Mario
An espresso pulled into a clear glass — visual stand-in for the difference between something that runs once and something that gets re-pulled every time you open the door. Photo by Nathan Dumlao on Unsplash.
Intermediate SwiftUI Observation

@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.

M
Mario
Close-up of a circuit board — a visual stand-in for the structured grid you get when you stop letting an LLM hand you a free-form string. Photo by Alexandre Debiève on Unsplash.
Intermediate Foundation Models Generable

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'.

M
Mario
A robotic hand against a soft blue background — visual stand-in for the on-device model that now ships with every iPhone running iOS 26. Photo by Possessed Photography on Unsplash.
Intermediate Foundation Models On-Device AI

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.

M
Mario
Wires and cables organized into clean bundles — visual stand-in for the cleanup that the Observation framework brings to SwiftUI state.
Intermediate Swift Observation

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.

M
Mario
Frosted glass surface with soft refracted light — visual stand-in for the new Liquid Glass material iOS 26 ships across system tooltips and overlays.
Intro iOS 26 TipKit

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.

M
Mario
Close-up of a colorful mosaic tile design — many small colored tiles arranged into a single pattern. Photo by Iryna Muller via Unsplash.
Intro iOS 26 Icon Composer

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.

M
Mario
A triangular glass prism on a white surface, refracting sunlight into a soft rainbow spectrum. Photo by Artem (renzu_melody) via Unsplash.
Intermediate SwiftUI Liquid Glass

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.

M
Mario
iPhone catching warm sunrays through frosted light — the ambient feel of iOS 26 Liquid Glass surfaces. Photo by chico__fotografo via Unsplash.
Intermediate SwiftUI Liquid Glass

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.

M
Mario
Code on a turned-on monitor — the migration view. Photo via Unsplash.
Intermediate Swift Concurrency

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.

M
Mario
Two directional signs at a fork — pick the right path. Photo by Phil Hearing via Unsplash.
Intermediate Swift Concurrency

@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.

M
Mario
Close-up of source code in a color-syntax editor, sharply foreshortened — photo by Patrick Martin via Unsplash
Intermediate Swift Concurrency

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.

M
Mario
NEWSLETTER

Don't miss a field note.

One email a week, the moment a new note ships. Honest iOS dev write-ups from production — Swift, SwiftUI, SwiftData, CloudKit, the whole Apple stack.

  • Weekly. Skim-friendly. Always free.
  • Real bugs, real fixes, real tradeoffs.
  • No tutorials, no AI filler, no course pitches.

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