Instruments profiling
Hub › iOS › Advanced › Instruments profiling
Goal
Use Xcode's Instruments to profile the app — capture a Time Profiler trace, find slow code, and detect memory leaks with the Leaks instrument.
Prerequisites
Launch Instruments
Three ways to start:
- Xcode: Product → Profile (Cmd+I) → choose a template
- Terminal:
xcrun instruments(CLI mode) - Standalone: Open Instruments.app from Xcode → Open Developer Tool
Time Profiler
The Time Profiler samples the call stack every 1ms and shows which functions consume the most CPU time.
Common findings in a SwiftUI app
| Symptom | Likely cause |
|---|---|
High body evaluation time | Unnecessary view recomputation — add EquatableView or @ViewBuilder |
JSONDecoder.decode at top | Fetching and decoding on the main actor — move to background Task |
modelContext.save() spikes | Saving after every toggle — batch saves or debounce |
Profile the list
- Select Time Profiler template
- Tap the record button (red circle)
- Scroll through the list several times
- Stop recording
The call tree shows that ItemViewModel.load() and JSONDecoder.decode(_:from:) are the top consumers. This confirms you should fetch on a background actor.
Fix: move network fetch to a ModelActor
Create DataFetcher.swift:
import SwiftData
@ModelActor
actor DataFetcher {
func fetchAndCache() async throws {
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
let (data, _) = try await URLSession.shared.data(from: url)
let remote = try JSONDecoder().decode([RemoteItem].self, from: data)
for item in remote {
let descriptor = FetchDescriptor<CachedItem>(
predicate: #Predicate { $0.id == item.id }
)
if try modelContext.fetch(descriptor).first == nil {
modelContext.insert(
CachedItem(id: item.id, title: item.title, detail: item.body)
)
}
}
try modelContext.save()
}
}Call it from the ViewModel:
let fetcher = DataFetcher(modelContainer: modelContainer)
try await fetcher.fetchAndCache()Profile again — the main thread CPU drops significantly.
Leaks instrument
- Select the Leaks template
- Record, then navigate through the app: sign in, favorite items, sign out, sign in again
- Stop
The Leaks instrument shows a timeline. Any red bars indicate leaked memory. Click a leak to see the retain cycle:
- Typical SwiftUI leak: a closure capturing
selfstrongly in a view modifier. - Fix: use
[weak self]in escaping closures, or prefer@ObservableoverObservableObject.
Memory Graph Debugger
For a quick ad-hoc check without Instruments:
- Run the app in Xcode
- Press the Memory Graph Debugger button (the three diamonds in the debug bar)
- Search for objects that should have been deallocated after a navigation pop
Checkpoint
- Time Profiler: network fetch no longer appears on the main thread.
- Leaks: zero red bars after 2 minutes of navigation.
- Memory Graph: no orphaned view models.
Next: TestFlight distribution — archive, export, and submit to App Store Connect.