Updated for Xcode 14.2
The body
property of any SwiftUI automatically gets the ability to return different views thanks to a special attributed called @ViewBuilder
. This is implemented using Swift’s result builder system, and it understands how to present two different views depending on our app’s state.
However, this same functionality isn’t automatically everywhere, which means any custom properties you make must return the same view type.
There are four ways you can fix this. The first option is to wrap your output in a group, so that no matter whether you send back an image or a text view they both go back in a group:
struct ContentView: View {
var tossResult: some View {
Group {
if Bool.random() {
Image("laser-show")
.resizable()
.scaledToFit()
} else {
Text("Better luck next time")
.font(.title)
}
}
.frame(width: 400, height: 300)
}
var body: some View {
VStack {
Text("Coin Flip")
.font(.largeTitle)
tossResult
}
}
}
Download this as an Xcode project
The second is to use a type-erased wrapper called AnyView
that we can return:
struct ContentView: View {
var tossResult: some View {
if Bool.random() {
return AnyView(Image("laser-show").resizable().scaledToFit())
} else {
return AnyView(Text("Better luck next time").font(.title))
}
}
var body: some View {
VStack {
Text("Coin Flip")
.font(.largeTitle)
tossResult
.frame(width: 400, height: 300)
}
}
}
Download this as an Xcode project
If you haven’t heard of this concept, it effectively forces Swift to forget about what specific type is inside the AnyView
, allowing them to look like they are the same thing. This has a performance cost, though, so don’t use it often.
Although both Group
and AnyView
achieve the same result for our layout, between the two it’s generally preferable to use Group
because it’s more efficient for SwiftUI.
A third option is to apply the @ViewBuilder
attribute yourself to any properties that need it, like this:
struct ContentView: View {
@ViewBuilder var tossResult: some View {
if Bool.random() {
Image("laser-show")
.resizable()
.scaledToFit()
} else {
Text("Better luck next time")
.font(.title)
}
}
var body: some View {
VStack {
Text("Coin Flip")
.font(.largeTitle)
tossResult
.frame(width: 400, height: 300)
}
}
}
Download this as an Xcode project
That works, but honestly if you find yourself reaching for @ViewBuilder
you should question whether you’re trying to put too much into one view.
The fourth solution, and the one that works best the majority of the time, is to break up your views into smaller views, then combine them together as needed:
struct TossResult: View {
var body: some View {
if Bool.random() {
Image("laser-show")
.resizable()
.scaledToFit()
} else {
Text("Better luck next time")
.font(.title)
}
}
}
struct ContentView: View {
var body: some View {
VStack {
Text("Coin Flip")
.font(.largeTitle)
TossResult()
.frame(width: 400, height: 300)
}
}
}
Download this as an Xcode project
This works particularly well to help break apart logic and layout, and also has the benefit of making your views more reusable elsewhere in your app. SwiftUI will automatically collapse your view hierarchy, so there is no meaningful performance difference when you break up a view.
SPONSORED From March 20th to 26th, you can join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer!
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.