SwiftUI View Tracking
The Embrace SDK provides tools to instrument SwiftUI view lifecycles and performance metrics, giving you insight into rendering behavior, visibility durations, and interactive readiness in SwiftUI apps.
How View Tracking Works
The SwiftUI view tracking system automatically generates OpenTelemetry spans based on critical lifecycle events:
Render loop performance - How efficiently your views update when SwiftUI's state changes trigger re-renders. This measures whether your view hierarchy is responding smoothly to data updates or if expensive computations in your view's body
are causing lag. Poor render loop performance typically shows up as stuttering animations or delayed responses to user taps.
View visibility - When views appear and disappear using SwiftUI's onAppear
and onDisappear
lifecycle events. This tracking captures navigation patterns and helps identify views that might be doing heavy work (like network calls or database queries) every time they become visible instead of loading data more efficiently.
Time to first render - How quickly users see your content after navigating to a new screen. This critical user experience metric measures the gap between when someone taps a button or link and when they see visual feedback that something is happening. Even showing a loading spinner quickly is better than a blank screen.
Time to content completion - When async content finishes loading (optional). This extends beyond first render to track when meaningful content like images, API responses, or computed data actually appears on screen. You can instrument this manually by marking when your async operations complete.
View.body execution time - Performance of your view building logic. Since SwiftUI calls the body
property frequently as part of its diffing algorithm, expensive operations here directly impact your app's frame rate. This includes complex calculations, heavy view composition, or inefficient use of SwiftUI modifiers.
These traces help you understand how your SwiftUI views behave in the system's rendering pipeline and measure how long users wait to see or interact with your content. The spans create a timeline showing exactly where bottlenecks occur - whether it's slow network requests, inefficient view updates, or expensive rendering operations that are impacting the user experience.
SwiftUI Rendering Lifecycle
SwiftUI rendering is governed by a system-managed render loop that typically runs at 60 Hz (every ~16ms). When a frame deadline is missed—for example, if your body
takes too long to compute—the result is a hitch: a visual stutter that users notice.
Embrace captures the following key phases of the SwiftUI render lifecycle:
Render Loop Spans
Each update to a SwiftUI view creates a render loop trace with this structure:
-
emb-swiftui.view.<name>.render-loop
Root span - Measures duration from view evaluation to the next run loop tick-
emb-swiftui.view.<name>.body
Child span - Duration of SwiftUI body evaluation (building the view hierarchy) -
emb-swiftui.view.<name>.appear
Child span - Time spent inside the.onAppear
view modifier -
emb-swiftui.view.<name>.disappear
Child span - Time spent inside the.onDisappear
view modifiernoteWhen
onDisappear
happens outside of a render loop, thedisappear
span will be a root span instead of a child span of the render-loop.
-
Time to First Render
emb-swiftui.view.<name>.time-to-first-render
Measures how long it takes from when the system first initializes the view until it appears onscreen
Time to First Content Complete (Optional)
-
emb-swiftui.view.<name>.time-to-first-content-complete
Tracks duration from view creation until a flag (typically a Boolean or otherEquatable
value) indicates the view has finished rendering meaningful content📝 Note: Not available when using the macro-based API
Configuration Options
You can add view instrumentation using any of these three approaches:
Custom Configuration: If you're not using the Embrace Dashboard, enable this by passing a custom EmbraceConfigurable
with isSwiftUiViewInstrumentationEnabled
set to true
when initializing the SDK.
1. Macro (@EmbraceTrace
) - Simplest
The easiest way to add view instrumentation—just annotate your struct with no additional configuration needed:
import EmbraceMacros
@EmbraceTrace
struct MyView: View {
var body: some View {
Text("Hello World")
}
}
Macro Limitations: The macro cannot currently accept parameters like name
, attributes
, or contentComplete
. Use the modifier or wrapper for customization.
Setup Required: Ensure you have added the EmbraceMacros
module to your application.
The macro is only available through Swift Package Manager.
2. View Modifier (.embraceTrace
) - Most Flexible
Apply view tracing by adding a modifier to any view.
Parameters
- name (required): Automatically detected in macro.
- attributes (optional): Custom attributes to add additional context to the created spans.
- contentComplete (optional): An
Equatable
value that triggers the end of the "time to first content complete" span when changed. Behaves the same as theonChange
view modifier.
struct MyView: View {
@State var loaded = false
var body: some View {
VStack {
Text("Hello")
Button("Finish Load") {
loaded = true
}
}
.embraceTrace(
"MyView", // Custom name
attributes: ["user_role": "admin"], // Optional metadata
contentComplete: loaded // Optional completion flag
)
}
}
3. View Wrapper (EmbraceTraceView
) - Alternative Syntax
Apply view tracing by wrapping your view:
struct MyView: View {
@State var loaded = false
var body: some View {
EmbraceTraceView(
"MyView", // Custom name
attributes: ["user_role": "admin"], // Optional metadata
contentComplete: loaded // Optional completion flag
) {
VStack {
Text("Hello")
Button("Finish Load") {
loaded = true
}
}
}
}
}
Best Practices
- Start simple: Use the macro (
@EmbraceTrace
) for basic tracking, then upgrade to the modifier when you need custom names or attributes - Track meaningful completion: Use the
contentComplete
parameter for views that load data asynchronously to measure when users can actually interact with your content - Avoid over-instrumentation: Do not nest multiple instrumented views unnecessarily—each generates its own trace tree and can impact performance
Common Use Cases
- Find layout bottlenecks - Identify slow
body
render spans that cause UI hitches - Measure screen load times - Track how long users wait to see your SwiftUI screens
- Reduce user abandonment - Understand when users leave before content finishes loading
- Compare view performance - Benchmark different view hierarchies and implementation approaches