Static Types in SwiftUI
When SwiftUI was announced, one of the early details that caught our attention was the use of static types to optimize parts of the view graph that can't change:
It's heavily inspired by the Elm Architecture and React. The biggest unusual (not sure if novel) thing is the use of the type system to optimize out the static subset of the view graph
— Joe Groff, on Twitter.
To see what this means in practice, let's consider the following example:
let stack = VStack {
Text("Hello")
Rectangle()
.fill(myState ? Color.red : Color.green)
.frame(width: 100, height: 100)
}
If we ask Xcode for the type of this view, we get a type that contains some View
. This is helpful when reading and writing code, but for our purposes we want the real underlying type, without some
. We can get the full type by putting the expression above in a variable and then using dump
, or by using a mirror:
print(Mirror(reflecting: stack).subjectType)
// output: VStack, _FrameLayout>)>>
To quickly see what's going on, we can visualize the type as a diagram:
The type of an expression in Swift is constant: it is computed at compile time. During the lifetime of our SwiftUI program, we can change state variables, observe our model, and render complicated view trees. During each update SwiftUI recomputes the view tree; it then uses the new view tree to update the screen, for instance, inserting or removing views. One thing that doesn't change is the type of our view tree.
To see why this matters, let's consider some frameworks similar to SwiftUI.
In our implementation of The Elm Architecture, we do something similar: when the state changes, we re-render our view tree. However, our implementation also includes a diffing step, effectively diffing at tree-level to add and remove views, and update any necessary properties. This approach is common to frameworks like React, languages like Elm, and similar approaches. It can feel wasteful, especially if only a single property has changed.
In SwiftUI, the implementation works differently. In our stack
above, SwiftUI knows the type: a vertical stack view with two subviews. During the execution of the program this type will never change — it's a static property of the code. As such, our program will always render a vertical stack view with a text and a rectangle. When the state changes, some of the views' properties might change, but the stack view with the two subviews will always persist.
This hard guarantee from the type system means that SwiftUI doesn't need to do a tree diff. Instead, it only needs to look at the properties of each view, and update those on screen. Theoretically, this still involves walking the entire tree, but walking a tree has a much lower complexity than diffing a tree.
Of course, many applications can't be written with a fully static view tree: perhaps you want to display items conditionally, or display a list of items (with a variable length), or even display completely different views depending on the state.
Let's look at these three in detail.
Optional Views
When we want to optionally display a view (based on some state), we can write an if-condition in SwiftUI:
let stack = VStack {
if myState {
Text("Hello")
}
Rectangle()
.fill(myState ? Color.red : Color.green)
.frame(width: 100, height: 100)
}
The type of stack
has now changed:
`VStack<TupleView<(Text?, ModifiedContent<_ShapeView<Rectangle, Color>, _FrameLayout>)>>`
The Text
type has also changed, to Text?
. At each render pass, SwiftUI needs to decide whether to insert, remove, or update that Text
. (Note that it doesn't need to figure out the position of Text
in the stack, it's statically encoded in the type).
Variable-Length Views
For view trees that have a variable length, SwiftUI uses ForEach
. We won't go into detail on ForEach
, but SwiftUI requires you to provide either a constant range, or, if the length is truly dynamic, to use an identifier for each element you're displaying. When the elements change, ForEach
uses the identifier to uniquely identify elements during a diffing step.
Different Views
Sometimes, you want to display completely different views based on the state. Inside a ViewBuilder
you can use an if-else
with differently typed branches:
let stack = VStack {
if myState {
Text("Hello")
} else {
Rectangle()
.fill(myState ? Color.red : Color.green)
.frame(width: 100, height: 100)
}
}
The type of the expression above is:
`VStack<_ConditionalContent<Text, ModifiedContent<_ShapeView<Rectangle, Color>, _FrameLayout>>>`
This means that SwiftUI will either display the left branch of the conditional content, or the right branch. When you can, use an if
or if-else
statement to convey as much information to the type system as possible. Outside of a view builder, you might need to wrap your views inside another layer to achieve this.
Sometimes, it's really hard or impossible to know the types up front. For example, if you display a news feed with different items, the types of the views might depend on data coming from the network. For these cases, there's a last resort: AnyView
. For example, while it's impossible to create an array of some View
, you can wrap each news item inside an AnyView
, and then create an [AnyView]
.
An AnyView
is a type-erased view, and as such, it provides no information at compile-time about what's inside. SwiftUI will need to do more work at runtime to verify changes (as mentioned here, and here).
Conclusion
There's a very deep connection between the type system and the efficiency of view rendering in SwiftUI. Understanding the connection will help you write more efficient code.
If you’ve enjoyed this insight, our weekly Swift Talk video series explores SwiftUI in more depth. In our latest public episode, we explore how to build a shake animation — which is less straightforward than it seems!
To learn with us, become a Subscriber.