Search code examples
iosswiftswiftui

Optional @ViewBuilder closure


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.


Solution

  • 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")
            })
        }
    }