Posts
Short-form iOS engineering insights — Swift, SwiftUI & the Apple platform, written from a senior perspective.
All posts
37Dark mode regressions don’t show up as crashes—they ship as unreadable text on blurred cards and brand tints that vanish over images
Dark mode regressions don’t show up as crashes—they ship as unreadable text on blurred cards and brand tints that vanish over images. Most teams compute contrast again...
A feed that jitters on device but “looks fine” in Simulator is often observation, not layout
A feed that jitters on device but “looks fine” in Simulator is often observation, not layout. I traced a scroll hitch to a single @Observable retained by two ancestors...
Stacks and geometry hacks can introduce scroll jank; a purpose-built SwiftUI `Layout` can centralize the math and make results more predictable
Stacks and geometry hacks can introduce scroll jank; a purpose-built SwiftUI Layout can centralize the math and make results more predictable. Measure once,
Cold starts regress, frames drop, and the trace looks “fine
Cold starts regress, frames drop, and the trace looks “fine.” Without precise, low-overhead markers on hot paths, you’re debugging timestamps and gut feel instead of t...
Shortcuts shows one dialog, your logs insist another
Shortcuts shows one dialog, your logs insist another? You may be attached to the wrong process—or hitting @Parameter/EntityQuery timing differences. Trust what the Sho...
Sign in with Apple outages during a domain change rarely come from Swift—they come from config
Sign in with Apple outages during a domain change rarely come from Swift—they come from config. And with Apple unifying relay addresses under private.icloud.com, auth ...
On visionOS, navigation issues often show up at state boundaries—not just buttons
On visionOS, navigation issues often show up at state boundaries—not just buttons. When 2D windows, 3D spaces, and restoration drift, back gestures and focus can feel ...
Toolchain bumps are an operational risk, not just a compiler upgrade — mismatched CI and local toolchains can introduce flaky PRs, signing issues, and stalled releases
Toolchain bumps are an operational risk, not just a compiler upgrade — mismatched CI and local toolchains can introduce flaky PRs, signing issues, and stalled releases...
Migrations to `SwiftUI` and structured concurrency often compile cleanly but can expose timing, cancellation, and partial-write bugs only at runtime — plan for an operational rollout phase
Migrations to SwiftUI and structured concurrency often compile cleanly but can expose timing, cancellation, and partial-write bugs only at runtime — plan for an operat...
Converting to modern SwiftUI often changes runtime behavior more than code size — cancellation, invalidation, and state restoration can surface in production if you treat this as a purely syntactic swap
Converting to modern SwiftUI often changes runtime behavior more than code size — cancellation, invalidation, and state restoration can surface in production if you tr...
Converting callback-heavy networking to Swift Concurrency can push cancellation, isolation, and scheduling issues into production — many of these problems only appear under real load
Converting callback-heavy networking to Swift Concurrency can push cancellation, isolation, and scheduling issues into production — many of these problems only appear ...
Renaming or reordering menus can silently break shortcuts, AppleScript/AppleEvent hooks, and assistive discovery — treat a menu redesign as a migration, not a string swap
Renaming or reordering menus can silently break shortcuts, AppleScript/AppleEvent hooks, and assistive discovery — treat a menu redesign as a migration, not a string s...
A single shared observable can kill scroll performance: one mutation, many ancestors re-render, stuttery scrolling and layout shifts
A single shared observable can kill scroll performance: one mutation, many ancestors re-render, stuttery scrolling and layout shifts. Read \(.sizeCategory) as clos...
Platform betas break timing and lifecycle assumptions — surface those breaks in a controlled pipeline before customers do
Platform betas break timing and lifecycle assumptions — surface those breaks in a controlled pipeline before customers do. Run a parallel CI lane (beta Xcode image...
Converting callbacks to `async`/`await` is a semantics change — not just a syntax rewrite
Converting callbacks to async/await is a semantics change — not just a syntax rewrite. Cancellation, lifetimes, and concurrency boundaries can change runtime behavior ...
Widening visibility in Swift packages can expand your ABI and increase the risk of small refactors causing runtime or binary incompatibilities
Widening visibility in Swift packages can expand your ABI and increase the risk of small refactors causing runtime or binary incompatibilities. Prefer internal by ...
Short guidance gaps at crowded race starts can come from coaching logic running off-watch—phone or network dependencies can break continuity
Short guidance gaps at crowded race starts can come from coaching logic running off-watch—phone or network dependencies can break continuity. Make the watch the source...
A SwiftUI canvas that looks perfect but VoiceOver only reads “group” usually means accessibility semantics or notifications were never made explicit — pixels aren’t the whole contract
A SwiftUI canvas that looks perfect but VoiceOver only reads “group” usually means accessibility semantics or notifications were never made explicit — pixels aren’t th...
Cross-process intent decoding can silently break widget updates — treat `AppIntent` parameters as a durable API
Cross-process intent decoding can silently break widget updates — treat AppIntent parameters as a durable API. Prefer concrete types for intent parameters (Int, en...
A frozen macOS window during a customer demo erodes trust instantly — many stubborn hangs only reproduce on device because the simulator’s environment differs
A frozen macOS window during a customer demo erodes trust instantly — many stubborn hangs only reproduce on device because the simulator’s environment differs. Sta...
RealityKit scenes that behave in short tests can degrade in long sessions — rising memory, thermal throttling, or process termination often surface after sustained use
RealityKit scenes that behave in short tests can degrade in long sessions — rising memory, thermal throttling, or process termination often surface after sustained use...
Converting free-text intent slots to `AppIntent` often pushes parsing errors from QA into production — misrouted intents, misclassifications, and new support cases can appear quickly
Converting free-text intent slots to AppIntent often pushes parsing errors from QA into production — misrouted intents, misclassifications, and new support cases can a...
Silently divergent local build scripts once caused a release failure for me — version your build-time tools and move them out of per-developer shell scripts
Silently divergent local build scripts once caused a release failure for me — version your build-time tools and move them out of per-developer shell scripts. Put g...
High-cardinality ad-hoc logs can bury real latency and causal boundaries — use `OSSignposter` to emit bounded, platform-visible intervals instead
High-cardinality ad-hoc logs can bury real latency and causal boundaries — use OSSignposter to emit bounded, platform-visible intervals instead. Create one OSSignp...
Implicit environment values can hide costly dependencies — multiple ancestors injecting the same observable can lead to unexpected re-renders and harder debugging
Implicit environment values can hide costly dependencies — multiple ancestors injecting the same observable can lead to unexpected re-renders and harder debugging. Pr...
Shortcuts that “do nothing” usually hide preventable failures: intent resolution misses, missing prompts, or long work killed silently — make them diagnosable, not mysterious
Shortcuts that “do nothing” usually hide preventable failures: intent resolution misses, missing prompts, or long work killed silently — make them diagnosable, not mys...
Memory growth during long sessions often points to an ownership leak, not a rendering bug — catch it with repeatable `Xcode Memory Graph` snapshots and diffs before assuming layout is the culprit
Memory growth during long sessions often points to an ownership leak, not a rendering bug — catch it with repeatable Xcode Memory Graph snapshots and diffs before assu...
Parsing and type‑checking — not linking — is often what makes CI builds stall
Parsing and type‑checking — not linking — is often what makes CI builds stall. Stale or mismatched .swiftmodule files multiply that pain. Prebuild framework module...
Visual regressions at accessibility text sizes keep slipping into releases — clipped labels and truncated cells are often predictable and preventable
Visual regressions at accessibility text sizes keep slipping into releases — clipped labels and truncated cells are often predictable and preventable. Run automated Dy...
Modifiers that capture mutable state can cause UI surprises under navigation and `List` reuse — treat them as small library contracts, not syntactic sugar
Modifiers that capture mutable state can cause UI surprises under navigation and List reuse — treat them as small library contracts, not syntactic sugar. Define Vi...
Ad‑hoc frame math sprinkled through view bodies breeds flaky, non‑deterministic layout
Ad‑hoc frame math sprinkled through view bodies breeds flaky, non‑deterministic layout. Pull spatial rules into focused Layout types to get deterministic sizing and ea...
A tiny change to `body` or to state ownership can raise per-frame CPU on lower-end devices and turn a calm rollout into an urgent rollback
A tiny change to body or to state ownership can raise per-frame CPU on lower-end devices and turn a calm rollout into an urgent rollback. Profile on representative ph...
Converting globals to explicit DI often surfaces production-only failures: blank screens, duplicated network calls, or stale `ObservableObject` state that don't crash but lengthen incident time
Converting globals to explicit DI often surfaces production-only failures: blank screens, duplicated network calls, or stale ObservableObject state that don’t crash bu...
Converting completion-handler telemetry to async/await can change cancellation and lifecycle semantics — and that can increase the risk of leaking data or leaving on-disk batches after consent changes
Converting completion-handler telemetry to async/await can change cancellation and lifecycle semantics — and that can increase the risk of leaking data or leaving on-d...
If your codebase still uses URLSession completion handlers, moving to Swift async/await can simplify call sites — but it also changes how cancellation, error propagation, and observability are handled
If your codebase still uses URLSession completion handlers, moving to Swift async/await can simplify call sites — but it also changes how cancellation, error propagati...
Stop treating migration from Combine to async/await as a mechanical find‑and‑replace — it’s a different execution model that affects timing, cancellation, and demand semantics
Stop treating migration from Combine to async/await as a mechanical find‑and‑replace — it’s a different execution model that affects timing, cancellation, and demand s...
A tangled web of push/pop calls and segues can slow teams down — but you can migrate to state‑driven navigation incrementally without a big rewrite
A tangled web of push/pop calls and segues can slow teams down — but you can migrate to state‑driven navigation incrementally without a big rewrite. Here’s a practical...