ViewBuilder vs. AnyView
One of the major benefits of SwiftUI is its amazing DSL. A feature that not only makes it nice to read but also makes it possible to use the type system to unlock tremendous performance benefits. AnyView
is a view that defies all of this, and although useful in very specific circumstances, you should always try to use a better alternative.
Let’s explore how all this ties together.
One major difference of SwiftUI with other declarative UI frameworks is how it uses the power of Swift at its maximum. Swift is a statically typed language, and that opens the door to a lot of interesting techniques with real benefits.
When you create a view hierarchy in SwiftUI, it is not only encoded at runtime with a tree of instances, it’s also encoded on the type system. The framework uses views with generic type parameters to define the view hierarchy.
Watch What’s behind SwiftUI DSL? - Swift Function Builders to know more about SwiftUI DSL.
Having the view hierarchy encoded in the type system lets the framework do a better job at diffing. Diffing happens when the UI needs to change. A declarative UI frameworks takes the new state of the UI, compares it with the previous one, and just performs the smaller amount of changes necessary.
It’s easy to imagine how having more information available can help with this. If each decision point on a hierarchy has its own type, it means that the framework only needs to do runtime checks for the ones that can change, and even in those cases having a specific type for each case makes it easier to optimise.
To illustrate this behaviour, let’s define a simple view and analyse its type.
var body: some View {
VStack {
Text("Hello")
Image(systemName: "square")
}
}
To easily see the type, you can just change the return type to an Int and read the compiler error.
Cannot convert return expression of type 'VStack<TupleView<(Text, Image)>>' to return type 'Int'
You can see how the view hierarchy is encoded in the view’s type using generic parameters. We have a VStack
with a TupleView
that has a Text
and an Image
. We can imagine how with this information the framework could be able to know with no runtime check that the hierarchy is not going to change, just the content of the leaf views.
That’s nice, but having to provoke an error to see the type is quite annoying. We can implement a small extension to print out the type of a view.
extension View {
func debugType() -> some View {
print(type(of: self))
return self
}
}
Adding this let us see the type of the view in the console. Much better.
But let’s focus again on the return type. That some View
. That’s Swift’s opaque return types feature being tremendously useful. Without that, having a framework like SwiftUI that creates such complex types would be impossible to work with.
Watch Swift Opaque Result Types to learn more about this feature.
some View
makes it so we don’t have to know any of this internal view types and avoids a lot of boilerplate. But is crucial to understand that the type is fixed at compile time, and it must be one concrete type. You can’t have multiple returns with different types, it must be always one type. In other words, opaque types don’t add dynamism into the system. The function has a static type as always, it’s just that you don’t have to write it yourself.
And this is problematic on some occasions, for example, when you want to return two different views based on some condition.
func labelOrText() -> some View {
if showLabel {
return Label("Bye", systemImage: "circle")
} else {
return Text("Bye")
}
}
This code will give you the following compiler error:
Function declares an opaque return type, but the return statements in its body do not have matching underlying types
Type erasers and AnyView
If you really want to have multiple types in a such a strongly typed system, there is always a scape hatch: type erasers. This is a technique that deserves its own post but suffices to say that they are quite common in Swift. The type eraser for SwiftUI views is called AnyView
.
By wrapping a view in an AnyView
we erase the underlying genuine type of the view. The compiler doesn’t know anymore what was there, a Text
? an Image
? No way to know. That may seem bad, but it gives us the benefit of mixing different views.
func labelOrText() -> AnyView {
if showLabel {
return AnyView(Label("Bye", systemImage: "circle"))
} else {
return AnyView(Text("Bye"))
}
}
Although this works, it is far from ideal. By using type erasure, we’re denying useful information to the framework.
So let’s look at an alternative!
Function builders with conditions
Wanting to return multiple views based on a condition often required to use AnyView
in the beginning of SwiftUI. Thankfully, since Swift 5.3 function builders, the Swift feature that makes the DSL possible, added support for multiple control flow statements, including if conditions.
Watch Swift Function Builders deep dive for Swift 5.3 to see all the amazing things this feature can do.
Thanks to that you can use if
conditions in SwiftUI body
with no problem.
var body: some View {
if showLabel {
Label("Bye", systemImage: "circle")
} else {
Text("Bye")
}
}
But we’re still using some View
as the return type. So you may ask, how are we able to have multiple types? And that’s the magic, there are no multiple types, there is only a single return type. SwiftUI is using function builders to use another of its internal types, to encode the hierarchy in the type system, just as we saw before.
In this case the hierarchy has a condition, so it has a special view type for that:
_ConditionalContent<Label<Text, Image>, Text>
Use it in your own methods
Being able to use if
in the body
is nice, but as you can see, our previous problematic example was using a separate method. That’s when most people use AnyView
or move the code into the main body property. But there is a better way.
To use SwiftUI DSL a method must be marked with a special annotation: @ViewBuilder
. By default, a view’s body
property is marked with it, that’s why we can use the DSL without by default.
But when we make our own function, we’re outside the DSL world. That’s why we were forced to write the return
statements and why the power of the if
didn’t work. But we can solve it easily! Just add the annotation yourself and you are back into the DSL world.
@ViewBuilder
func labelOrText() -> some View {
if showLabel {
Label("Bye", systemImage: "circle")
} else {
Text("Bye")
}
}
Conclusion
SwiftUI uses Swift’s powerful type system at its fullest. some View
helps us not to have to write the complicated types, but it doesn’t give us dynamism. Using AnyView
denies that, so it’s better to avoid it if possible. Just use @ViewBuilder
when you are outside a view’s body
and you will be ready. That said, sometimes is still needed, so don’t be afraid of it. Just know the tools you have at your disposal and pick the best for the job.