Avoiding SwiftUI’s AnyView
Discover page available: SwiftUISwiftUI ships with a special view called AnyView
, which can be used as a type erased wrapper to enable multiple view types to be returned from a single function or computed property, or to let us reference a view without having to know its underlying type.
However, while there are cases in which we might need to use AnyView
, it’s often best to avoid it as much as possible. That’s because SwiftUI uses a type-based algorithm to determine when a given view should be redrawn on screen, and since two AnyView
-wrapped views will always look completely identical from the type system’s perspective (even if their underlying, wrapped types are different), performing this kind of type erasure significantly reduces SwiftUI’s ability to efficiently update our views.
So, in this article, let’s take a look at two core techniques that can help us avoid AnyView
while still enabling us to work with multiple view types in very dynamic ways.
Handling multiple return types
When using SwiftUI to build views, we very often use the some View
opaque return type to avoid having to explicitly define what exact type that we’re actually returning. That’s especially useful since (almost) every time that we apply a modifier to a given view, or change the contents of a container, we’re actually changing the type of view that we’ll return.
However, the compiler is only able to infer the underlying return type when all of the code branches within a given function or computed property return the exact same type. So something like the following won’t compile, since the if
and else
branches within our textView
property return different types of views:
struct FolderInfoView: View {
@Binding var folder: Folder
var isEditable: Bool
var body: some View {
HStack {
Image(systemName: "folder")
textView
}
}
private var textView: some View {
// Error: Function declares an opaque return type, but
// the return statements in its body do not have matching
// underlying types.
if isEditable {
return TextField("Name", text: $folder.name)
} else {
return Text(folder.name)
}
}
}
Initially, it might seem like the above is one of those situations in which AnyView
must be used in order to give all of our code branches the same return type. What’s very interesting, though, is that if we instead place the above conditional expression inline within our body
property, the compiler error goes away:
struct FolderInfoView: View {
@Binding var folder: Folder
var isEditable: Bool
var body: some View {
HStack {
Image(systemName: "folder")
if isEditable {
TextField("Name", text: $folder.name)
} else {
Text(folder.name)
}
}
}
}
That’s because SwiftUI uses a function/result builder to combine all of the views that are defined within a given scope (such as the above HStack
) into a single return type, and the good news is that we can use that same builder type within our own properties and functions as well.
Like we took a look at in “Adding SwiftUI’s ViewBuilder attribute to functions”, all that we have to do to utilize that same powerful view building functionality is to use the @ViewBuilder
attribute — which in turn lets us express multiple types of views within the same scope, like this:
struct FolderInfoView: View {
@Binding var folder: Folder
var isEditable: Bool
var body: some View {
HStack {
Image(systemName: "folder")
textView
}
}
@ViewBuilder
private var textView: some View {
if isEditable {
TextField("Name", text: $folder.name)
} else {
Text(folder.name)
}
}
}
Note how we’re no longer using any return
statements within our new textView
property implementation, since each expression will now be parsed by SwiftUI’s ViewBuilder
, rather than being returned separately.
So the first way that AnyView
can often be avoided is by using the ViewBuilder
attribute whenever we want a given property or function to be able to return multiple view types.
Generic view properties
Another really common type of situation in which AnyView
is often used is when we want to store a given view in a property without having to know its exact type. For example, let’s say that we’re working on the following ItemRow
, which currently uses AnyView
to enable us to inject any accessoryView
that we want to display at the trailing edge:
struct ItemRow: View {
var title: String
var description: String
var accessoryView: AnyView
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(title).bold()
Text(description)
}
Spacer()
accessoryView
}
}
}
Since we can’t use the some View
opaque return type for stored properties (they’re not returning anything, after all), and since we’re no longer dealing with a predefined number of views that can be combined using ViewBuilder
, we’ll have to explore another strategy if we’d like to remove our usage of AnyView
in this case.
Just like how we previously took inspiration from SwiftUI itself when using ViewBuilder
, let’s do the same thing here. The way that SwiftUI solves the problem of enabling any view to be injected is by making the host view generic over the type of view that it’ll contain. For example, the built-in HStack
container is defined as a generic that has a Content
type, which in turn is required to conform to the View
protocol:
struct HStack<Content>: View where Content: View {
...
}
Using the same kind of generic type constraint, we can make our ItemRow
adopt the exact same pattern — which will let us directly inject any View
-conforming type as our accessoryView
:
struct ItemRow<Accessory: View>: View {
var title: String
var description: String
var accessoryView: Accessory
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(title).bold()
Text(description)
}
Spacer()
accessoryView
}
}
}
Not only does the above give us better performance during view updates (since all of the types involved are now well-defined and transparent to the type system), it also makes our call sites simpler as well, since each accessoryView
no longer has to be manually wrapped within an AnyView
:
// Before:
ItemRow(
title: title,
description: description,
accessoryView: AnyView(Image(
systemName: "checkmark.circle"
))
)
// After:
ItemRow(
title: title,
description: description,
accessoryView: Image(
systemName: "checkmark.circle"
)
)
Conclusion
While SwiftUI makes many aspects of UI development simpler, there’s no denying that it’s an incredibly complicated framework that makes heavy use of some of Swift’s most powerful features. So while it might be easy to get started building views using it, we often have to use quite advanced techniques (like generic programming) in order to make the best use of what SwiftUI has to offer.
Of course, just because it might be a good idea to avoid AnyView
as much as possible doesn’t mean that it should never be used. It’s a part of SwiftUI’s public API for a reason, and the above two techniques won’t work in every single situation — but when they do, they’ll often result in much more elegant and efficient code.
What do you think? Will the above techniques help you remove any usages of AnyView
? Let me know, along with your questions or comments, either via Twitter or email.
Thanks for reading!