-
Notifications
You must be signed in to change notification settings - Fork 0
Book 20 Performance Instruments And Best Practices
Part V — Advanced Techniques · Claude's Xcode 26 Swift Bible
← Book-19-Building-Custom-Views-And-Modifiers · Chapters and Appendices · Book-21-Git-And-GitHub →
Claude's Xcode 26 Swift Bible -- Part V: Advanced Techniques
Two production apps in this book's roster put the patterns in this chapter to work continuously. CryoTunes Player (github.com/fluhartyml/CryoTunesPlayer) handles streaming MusicKit playback while keeping a SwiftUI animated transport row at 60fps — a real test of the actor-contention and
@StateObjectpatterns this chapter covers. See Source Tour 18. Tally Matrix Clock (github.com/fluhartyml/Tally-Matrix-Clock) re-renders an animated grid of squares every second on tvOS, where focus-engine performance and SwiftUI body churn matter more than on iPhone. See Source Tour 19.
Two things changed in the X26 era that matter for this chapter. Instruments shipped a serious refresh[[p1]], and Swift 6.2 (then 6.3) added a handful of language features aimed straight at performance-sensitive code[[p2]]. Everything below in the rest of the chapter still stands — Time Profiler, Allocations, the SwiftUI instrument, the concurrency patterns. Treat what's in this section as the X26 layer that sits on top of those fundamentals.
-
Power Profiler — tells you where the watt-hours go. Run it against an iOS or iPadOS device and you'll see a per-subsystem breakdown for CPU, GPU, and networking, plus your app's contribution. You can either record a live trace from the Instruments library or open an
.atrcfile the device captured for you under Developer settings. - CPU Counters with Bottleneck Analysis — a guided wrapper over the raw CPU counter telemetry. The CPU Bottlenecks template sorts every cycle of sustainable CPU bandwidth into one of four buckets, named by Apple: Useful (the CPU made forward progress), Instruction Delivery (the front-end couldn't feed enough instructions), Instruction Processing (an instruction stalled waiting on data or its own cost), Discarded (a speculative path was taken and thrown away).
- Processor Trace — logs every function call the CPU executed. Expensive to record, and the trace files are big. The payoff is that when sampling-based tools shrug at your perf bug, processor trace can usually point at the function.
- Foundation Models Instrument — lines up against Book 22's framework work. It breaks an inference run into request timing, asset loading, prompt processing, and the inference step itself, so you can see which slice owns the latency.
- SwiftUI (next-gen) — the new SwiftUI template, replacing the original. It logs every update SwiftUI runs, draws a Cause & Effect Graph that traces why a body recomputed, and includes a "Show View Hierarchy" action so you can jump straight from the timeline to the offending subtree.
When a SwiftUI screen feels heavy, run the next-gen SwiftUI instrument before anything else. The Cause & Effect Graph names the source of each body recomputation: a state mutation you wrote, environment value churn, or an identity reset because a ForEach is missing a stable id. Three different problems with three different fixes — and the instrument labels them for you instead of leaving you to guess.
Swift 6.2 came with X26 at launch, and Swift 6.3 followed in Xcode 26.4. Three additions across those releases pull their weight in performance-sensitive code:
-
Span<T>andMutableSpan<T>— bounds-checked slice types. You can hand off a sub-range of a buffer without copying it, and the compiler is the one watching the boundaries instead of you. Anywhere you reach forUnsafeBufferPointertoday is a candidate for replacement.MutableSpanis now valid as aninoutparameter without the experimental flag the early betas required. -
InlineArray<N, T>— a fixed-size array that lives on the stack instead of the heap. The use case is short, predictable data — a 3-component coordinate, a 16-byte hash, an RGBA pixel — where the heap allocation was always overkill.
struct Point {
var coordinates: InlineArray<3, Double>
}-
@concurrentannotation — the explicit way to say "run this on the concurrent thread pool, not on whatever actor my caller is sitting on." It hands isolation off intentionally instead of letting Swift's default rules infer it.
@concurrent
func processData() async {
// Runs on concurrent thread pool, not caller's context
}Read @concurrent as the mirror image of @MainActor. @MainActor says "this stays on main." @concurrent says "this leaves." Spelling that out in the signature beats relying on Swift's default isolation inference, especially in code where the wrong choice costs you a frame.
Data.bytes hands you a Span<UInt8> over the raw bytes of a Data value. For read-only byte access this is the new shape: no closure, no pointer juggling, no withUnsafeBytes wrapper. Bounds safety is built into the type, so the entire family of off-by-one mistakes that used to live in this code path is gone before runtime.
[p1] Apple, *Xcode 26 Release Notes* and *Xcode 26.4 Release Notes*. [developer.apple.com/documentation/xcode-release-notes/xcode-26-release-notes](https://developer.apple.com/documentation/xcode-release-notes/xcode-26-release-notes), [xcode-26_4-release-notes](https://developer.apple.com/documentation/xcode-release-notes/xcode-26_4-release-notes) — verified 2026-04-29.
[p2] Apartment Long-Term-Memory, `Swift-6-Quick-Reference-WWDC-2025.md` and `Xcode-26-Release-Timeline.md`, both verified against Apple release notes 2026-04-29.
This chapter covers launching Instruments from Xcode and reading the three most-used templates (Time Profiler, Allocations, SwiftUI), spotting and reducing unnecessary SwiftUI body recomputations, the correct use of @StateObject vs. @ObservedObject, keeping actors from becoming bottlenecks in Swift 6 concurrency, and a set of habits that keep an app smooth as it grows.
Don't profile before there's a problem. Guessing what's slow is usually wrong, and guessing-based changes make code harder to read without making it faster.
Start profiling when you see one of these:
- The UI stutters while scrolling, typing, or animating.
- The Xcode Debug Navigator shows CPU or memory climbing past what feels reasonable.
- A specific action (save, open, search) feels laggy.
Profile with real user scenarios on a real device, not the simulator. The simulator gives you roughly the host Mac's speed, which hides problems that bite on an actual iPhone.
From Xcode: Product > Profile (⌘I) with a physical device attached. Instruments opens the template picker. The three templates you use 95% of the time:
- Time Profiler -- where is the app spending CPU?
- Allocations -- what objects are being created, how many, and how long do they live?
- SwiftUI -- which views are re-rendering, and how often?
Start with Time Profiler when the symptom is "slow or stuttering." Start with SwiftUI when the symptom is "the screen flickers or my animations jump." Start with Allocations when memory is climbing.
Click Record, use the app the way the slow case uses it, click Stop. You get a flame-graph-style view: tall columns are where the CPU spent the most time.
- Call tree in the bottom pane. Expand it by heaviest-first (right-click > "Invert Call Tree" if you want the hottest leaves at the top).
- Bold entries are your app's code. Grayed-out entries are Apple frameworks.
- Percentage column tells you what fraction of wall-clock time was spent in that function.
Look for code you wrote consuming a surprising percentage. If ContentView.body is eating 40% of CPU during a scroll, that's your fix.
Switch the display to Flamegraph for a scrollable, visual summary. Wider bars = more time. Click a bar to jump to the source file.
The SwiftUI template records every view-body call. Two columns matter:
-
Call count -- how many times each view's
bodyran. - Duration -- total time spent in that body.
If a single Text row's body is being called thousands of times during a short interaction, something above it is re-rendering unnecessarily. That's almost always a fix in the parent: an observable-object update that's too coarse-grained, or a state change that shouldn't be triggering a repaint.
Rule: if the view creates the object, use @StateObject. If the object is handed in from outside, use @ObservedObject. @StateObject creates the object once and keeps it alive across re-renders; @ObservedObject assumes ownership lives elsewhere.
struct Child: View {
@StateObject private var model = Model() // created once, persists
var body: some View { Text(model.title) }
}The @Observable macro (Swift 5.9+) avoids this trap entirely:
@Observable
final class Model { var title = "" }
struct Child: View {
@State private var model = Model()
var body: some View { Text(model.title) }
}Pass the minimum properties a view needs. The fewer inputs a view has, the fewer reasons it has to re-render. ProfileCard(name: user.name) only re-renders when the name changes, where ProfileCard(user: user) re-renders on any change to user:
ProfileCard(name: user.name)Or split the model into smaller @Observable classes so only the relevant piece triggers updates.
List and ForEach rely on stable id values to animate inserts, deletes, and updates without re-rendering the whole list. Give every row a real identity:
ForEach(items, id: \.id) { item in Row(item: item) }Using id: \.self on mutable value types, or generating a fresh UUID on every render, defeats the identity check and forces a full redraw.
AsyncImage does no caching. If the user scrolls past an image, then back, it re-downloads. For serious image use, reach for a caching library (Kingfisher, Nuke) or cache manually in a model.
Swift 6's concurrency makes actors safe. It can also make them slow if a single actor is a choke point.
actor ImageCache {
private var cache: [URL: UIImage] = [:]
func image(for url: URL) async -> UIImage { /* load and store */ }
}If every view on screen asks the cache for its image, they all serialize through this one actor. The first one gets serviced fast, the rest queue up.
-
Narrow the
await. If the work you do on the actor is just "look up in a dictionary," let the actor return the cached value fast, but perform the expensive load off-actor. -
Split the actor. Two actors can't compete with each other, and each serves a different slice of the work.
-
Use
nonisolatedmethods for reads that don't touch mutable state. Those don't go through the actor's execution queue. -
Use
.taskwith IDs to cancel in-flight work. When a row scrolls offscreen, cancel the image load rather than letting it finish.
Habits that keep an app fast by default:
- Keep view hierarchies shallow. Deep nesting makes SwiftUI's diffing more expensive and makes the code harder to read.
- Pull state up only as far as it needs to go. State owned by a grandparent triggers more re-renders than state owned by the immediate parent.
-
Avoid expensive computations in
body. Compute once in a helper method (or outside the view) and pass the result in. -
Prefer
@ObservableoverObservableObject. Finer-grained tracking means fewer spurious re-renders. - Measure before and after. If you "optimized" something, run Instruments again to confirm. Most optimizations don't help; some make things worse.
A List that renders 10,000 rows smoothly, because it does the right things by default:
import SwiftUI
struct Row: Identifiable {
let id: Int
let title: String
let subtitle: String
}
struct BigList: View {
let rows: [Row] = (0..<10_000).map {
Row(id: $0, title: "Item \($0)", subtitle: "Subtitle \($0)")
}
var body: some View {
List(rows) { row in
HStack {
Text(row.title).font(.headline)
Spacer()
Text(row.subtitle).foregroundStyle(.secondary)
}
}
}
}Why this is fast:
-
Rowhas a stableid: Int.Listcan diff efficiently on inserts / deletes. - The row view has no
@ObservedObjector heavy computation. - We don't wrap the list in a
GeometryReaderor force it into a custom layout. - The strings are computed up front, not rebuilt in
body.
Run Instruments (Time Profiler) on this; scrolling should spend near-zero CPU. That's the target trace shape for a smooth list.
Part V closes with performance. Part VI is the toolchain beyond the code itself. Book 21 covers Git and GitHub -- how to version your work, share it with a remote, and participate in the standard open-source workflow.
← Book-19-Building-Custom-Views-And-Modifiers · Chapters and Appendices · Book-21-Git-And-GitHub →
Feedback: Found something off? Open an issue · Discuss it · Email Michael
Claude's X26 Swift6 Bible | GPL v3 | Built with Claude by Anthropic | Repo