Is it possible in SwiftUI to have an optional @ViewBuilder
closure? For example, let's say I want to develop a custom view that takes two view builder closures like this:
import SwiftUI
struct TopAndBottomView<Content>: View where Content: View {
let topContent: () -> Content
let bottomContent: () -> Content
init(@ViewBuilder topContent: @escaping () -> Content, @ViewBuilder bottomContent: @escaping () -> Content) {
self.topContent = topContent
self.bottomContent = bottomContent
}
var body: some View {
VStack {
topContent()
Spacer()
bottomContent()
}
}
}
struct TopAndBottomView_Previews: PreviewProvider {
static var previews: some View {
TopAndBottomView(topContent: {
Text("TOP")
}, bottomContent: {
Text("BOTTOM")
})
}
}
But I'd like the bottom view to be optional. I tried with:
struct TopAndBottomView<Content>: View where Content: View {
let topContent: () -> Content
let bottomContent: (() -> Content)?
init(@ViewBuilder topContent: @escaping () -> Content, @ViewBuilder bottomContent: (() -> Content)? = nil) {
self.topContent = topContent
self.bottomContent = bottomContent
}
var body: some View {
VStack {
topContent()
Spacer()
if bottomContent != nil {
bottomContent!()
}
}
}
}
but I get this error:
Function builder attribute 'ViewBuilder' can only be applied to a parameter of function type.
Thanks.
@JoeBayLD asked:
How would you do this if the topContent and bottomContent are different view types? I made a new generic property but when using the default 'nil' argument, any callers can't infer the content type
You can make both ViewBuilder parameters non-optional, and then handle the "no bottom content" case by making an extension where BottomContent == EmptyView
:
struct TopAndBottomView<TopContent: View, BottomContent: View>: View {
let topContent: TopContent
let bottomContent: BottomContent
init(@ViewBuilder topContent: () -> TopContent,
@ViewBuilder bottomContent: () -> BottomContent) {
self.topContent = topContent()
self.bottomContent = bottomContent()
}
var body: some View {
VStack {
topContent
Spacer()
bottomContent
}
}
}
extension TopAndBottomView where BottomContent == EmptyView {
init(@ViewBuilder topContent: () -> TopContent) {
self.init(topContent: topContent, bottomContent: { EmptyView() })
}
}
// usage
TopAndBottomView(topContent: { Text("hello") })
TopAndBottomView(topContent: { Text("hello") }, bottomContent: { Text("world") })
Taking into account buildIf
feature of ViewBuilder
the following approach is possible that allows to keep ViewBuilder
in init
(that is preferable)
Tested & works with Xcode 11.2 / iOS 13.2
struct TopAndBottomView<Content>: View where Content: View {
let topContent: () -> Content
let bottomContent: () -> Content?
init(@ViewBuilder topContent: @escaping () -> Content,
@ViewBuilder bottomContent: @escaping () -> Content? = { nil }) {
self.topContent = topContent
self.bottomContent = bottomContent
}
var body: some View {
VStack {
topContent()
Spacer()
bottomContent()
}
}
}
So works as this one
struct TopAndBottomView_Previews: PreviewProvider {
static var previews: some View {
TopAndBottomView(topContent: {
Text("TOP")
}, bottomContent: {
Text("BOTTOM")
})
}
}
and this one
struct TopAndBottomView_Previews: PreviewProvider {
static var previews: some View {
TopAndBottomView(topContent: {
Text("TOP")
})
}
}
In this fantastic post from Sundell, he suggests that we build a custom struct
Unwrap
to unwrap an optional value and turn it into a View
, the following code is what he did in that post:
import SwiftUI
/// # Unwrap
/// unwraps a value (of type `Value`) and turns it
/// into `some View` (== `Optional<Content>`).
struct Unwrap<Value, Content: View>: View {
private let value : Value? // value to be unwrapped
private let content: (Value) -> Content // closure: turn `Value` into `Content`
init(
_ value: Value?,
@ViewBuilder content: @escaping (Value) -> Content // ⭐️ @ViewBuilder
) {
self.value = value
self.content = content
}
var body: some View {
// map: (by the closure `content`)
// nil (Optional<Value>.none) -> nil (Optional<Content>.none)
// Optional<Value>.some(Value) -> Optional<Content>.some(Content)
value.map(content) // Optional<Content>
}
}
And then I wrote some code to demonstrate how we could use Unwrap
to construct our views:
import SwiftUI
// MyView
struct MyView: View {
@State private var isValue1Nil = false
@State private var isValue2Nil = false
var value1: Int? { isValue1Nil ? nil : 1}
var value2: Int? { isValue2Nil ? nil : 2}
var body: some View {
VStack {
// stack of `Unwrap`s
VStack {
// ⭐️ `Unwrap` used here.
Unwrap(value1) {
Color.red.overlay(Text("\($0)"))
}
Unwrap(value2) {
Color.orange.overlay(Text("\($0)"))
}
}.border(Color.blue, width: 3)
// toggles
HStack {
Toggle(isOn: $isValue1Nil) {
Text("value1 is nil")
}
Toggle(isOn: $isValue2Nil) {
Text("value2 is nil")
}
Spacer()
}
.padding()
.overlay(Rectangle().stroke(Color.gray, style: .init(dash: [6])))
} // VStack (container)
.padding()
.border(Color.gray, width: 3)
}
}
And the result is as follows:
----[ edited ]----
Or alternatively, we can make a View
extension to do the job:
// view.ifLet(_:then:)
extension View {
@ViewBuilder func ifLet<Value, Content: View>(
_ value: Value?,
@ViewBuilder then modifySelfWithValue: (Self, Value) -> Content
) -> some View {
if value != nil {
modifySelfWithValue(self, value!)
} else { self }
}
}
The following is another demo on how to use this extension:
struct ContentView: View {
@State private var isNil = false
var value: Int? { isNil ? nil : 2 }
var body: some View {
VStack {
Color.red.overlay(Text("1"))
// ⭐️ view.ifLet(_:then:)
.ifLet(value) { (thisView, value) in
// construct new view with `thisView` and `value`
VStack {
thisView
Color.orange.overlay(Text("\(value)"))
}
} // view modified by `ifLet`
.border(Color.blue, width: 3)
// toggles
Toggle(isOn: $isNil) { Text("value is nil") }
.padding()
.overlay(Rectangle().stroke(Color.gray, style: .init(dash: [6])))
} // VStack (container)
.padding()
.border(Color.gray, width: 3).frame(height: 300)
}
}
and the result is:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With