Pass SwiftUI view as argument to another view

I assume since you are reading this article that you want to be able to pass a view or multiple views as an argument to another view using SwiftUI. In this tutorial we will learn how we can use ViewBuilder to achieve this.

Step 1: Create a custom view

This custom view is going to be super basic. The custom view is going to contain a VStack that will have a red border. Add the following code to your project(All my code will be done in the ContentView):

struct BorderedView: View {
    var body: some View {
        VStack {
            Text("Bordered View")
        }.border(Color.red,
                 width: 2)
    }
}

I have updated my ContentView to look like this:

struct ContentView: View {
    var body: some View {
        BorderedView()
    }
}

If I build and run the app now, it looks like this:

Ok, now that our custom view works, let's update it to allow us to pass one or more views as an argument!

Step 2: Allow BorderedView to take views as argument/parameter

To do this we need to update the BorderedView to look like the following:

struct BorderedView<Content: View>: View {
    let content: Content
    
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
    
    var body: some View {
        VStack {
            self.content
        }.border(Color.red,
                 width: 2)
    }
}

Ok, so what have we done here?

We have added a content property. This property will work as any other property, but we are using a Content type. The reason for this is because we cannot use View directly. If we try to use View directly instead of using generics, we will get the following error:

Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements

Next, we have created an initialiser that will take a closure that returns a type of Content which we know needs to conform to the View protocol, but we also have @ViewBuilder.

What is @ViewBuilder? ViewBuilder is a functionBuilder, if you want to read up about these, you can take a look at this article by Vadim Bulavin.

Apple defines a ViewBuilder as:

A custom parameter attribute that constructs views from closures

This means that we can use the ViewBuilder parameter attribute on closure parameters/arguments to allow the close to provide multiple child views. See docs.

What other updates have we made to our custom view? We call the content closure when we assign it to our content property, and then we replaced the Text view in the VStack with the content property. This will allow us to use any views that gets passed in as arguments as child views of our VStack.

Step 3: Pass views to BorderedView

To see this in action, we need to update the ContentView code to look like the following:

struct ContentView: View {
    var body: some View {
        BorderedView {
            Text("Text view in CustomView")
            Button(action: {
                print("Button tapped")
            }, label: {
                Text("My button label")
            })
        }
    }
}

In the above code were are still using BorderedView, but this time we need to pass views in as arguments. For this example I have passed in two views, specifically a Text view, as well as a Button. If we build and run the app, we should see the following:

Conclusion

SwiftUI and ViewBuilder make it very easy to create custom views that can take other views as an argument. This can be super powerful when composing views.

If you want to see the full source code, you can find it here.