Creating Custom Container Views In SwiftUI

Posted in SwiftUI

Updated on March 20th, 2023

⏱ Reading Time: 6 mins

SwiftUI framework provides us with various kinds of views to use when implementing user interfaces. Some of them have a particular significance, as they act as containers for other SwiftUI views. Such containers are, for instance, the HStack, VStack, ForEach, LazyVGrid, LazyHGrid, and of course, a lot more. We use them all the time to build our views, but did you know that we can also create our own container views as well?

In this post you are going to find out everything you need to know in order to create your own container views. It would be interesting to say though, before going any further, that doing so is possible thanks to the ViewBuilder attribute; a special keyword which, according to the documentation, it allows us to create SwiftUI views from closures. I prompt you to read more on Apple docs here.

Note: You can find an older tutorial about how to use ViewBuilder attribute to create and return SwiftUI views from methods.

So, let’s explore everything demonstrating a simple scenario.

Defining the goal

For the sake of the demonstration, let’s suppose that the following SwiftUI view is a custom container; it’s going to display other SwiftUI views:

As you can find out in the above code snippet, the view is presented initially transparent and becomes fully opaque using an animation effect with a specific duration. It’s given a particular height, and in a ZStack there is a rounded rectangle filled with some color. On top of that, it’s going to display other views.

In addition, let’s consider the following simple views that provide some dummy content:

Finally, let’s also see the following, last one, view. It contains two buttons, and depending on the one that will be tapped, the proper content is given to the FadingContainer instance:

Whether the FadingContainer will be shown or not is up to the value of the contentType state property. By default is set to none and FadingContainer does not appear. When, however, gets any other value, the view redraws its content and also shows the FadingContainer along with the proper content specified in the closure.

But this is never going to work the way the above sample views are right now! What I’m showing here is actually the goal and the topic of this post; to make a custom view accept other views as arguments through a closure:

Let’s get down to making everything work.

The necessary additions to the custom container view

Right before going any further, it would be useful to clarify a detail that has probably become obvious already; the FadingContainer view will not be getting a SwiftUI view as argument directly, rather a closure that will return a SwiftUI view.

In order to make it capable of accepting such an argument, it’s necessary to declare a matching stored property in it first, so we can keep the input closure. Obviously, the type of that property should be the same to the provided argument; a closure returning a SwiftUI view as a value:

The above, however, is not a valid declaration. View is a protocol and not a concrete type such as a class or struct, so we cannot use it as so.

The solution here lies on turning FadingContainer into a generic type exactly like that:

Alternatively, we could also write this:

The Content generic type now refers to any SwiftUI view, and contrarily to the previous restriction, we are allowed to use this one just like every other concrete type. With that in mind, let’s properly declare the property that will store the provided closure returning any SwiftUI view in the FadingContainer struct:

That’s not all yet, though.

It’s also necessary to explicitly define an initializer method with one parameter value. That parameter value must be a closure just like the one declared above, but most importantly, we must prepend it with the @ViewBuilder attribute:

In the initializer’s body we assign the content parameter value to the content property declared in FadingContainer.

At this point FadingContainer can successfully accept and store any SwiftUI view as argument through the closure upon initialization. What it doesn’t still do though, is to show that view.

To change that, all we have to do is to replace the line that contains the following comment:

with a call to the closure that returns the SwiftUI view:

After having made all the above additions and changes, the following lines back in the MainView are going to work just fine, leading to the appearance of the one or the other view in the FadingContainer:

Providing other arguments to the container view

Along with a closure that returns a SwiftUI view, a custom container view can also accept any number of other arguments as well. To demonstrate that, let’s consider the following stored state property that keeps the animation duration regarding the view’s appearance:

This new property will get its value upon initialization, so we have to update the initializer and add a respective parameter. To avoid complexities when using FadingContainer, that new parameter will be the first in the list of parameters:

Of course, we should not forget to update the hardcoded animation duration value with the animationDuration we just defined:

With all that in place, we can now initialize FadingContainer providing both the desired animation duration and a closure with the view we want to show:

Providing other closures as arguments

Besides simple arguments, we can also supply a custom container view with additional arguments that are closures.

To see an example of that in the FadingContainer, let’s suppose that we want to add a dismiss button. On tap, it will be calling a closure. Back in the call site, we’ll trigger the disappearance of the FadingContainer view in the closure’s body.

Let’s take things step by step, starting with the declaration of the following (last one) stored property in the FadingContainer view. Along with that, let’s update the initializer method as well so the view can actually receive a closure matching to the dismiss action when it gets initialized:

Notice that the onDismiss parameter value is assigned to the local state property with the same name.

In the view’s body now let’s make the following small addition; a button that calls the onDismiss closure when it gets tapped:

Back to the call site, which in this case is the MainView demonstrated previously, we can now initialize the FadingContainer view passing three arguments; the animation duration time, the closure with any arbitrary SwiftUI content, and the closure to handle the dismiss action:

By setting none to contentType the FadingContainer view will not be displayed any more. Even though all these present a quite particular example, they clearly demonstrate how to provide more closures as arguments to any SwiftUI view that acts as a custom container view.

Conclusion

As you have found out by reading up until this point, it’s not difficult to create custom containers in SwiftUI and provide views as arguments to other views. All the magic happens with the ViewBuilder attribute in the initializer method and by making the container view generic, so it can declare closures having as return type any SwiftUI view. In addition to all that, you have also met how to provide other arguments as well, no matter if they are plain values or closures. Undeniably, the final result is similar to built-in SwiftUI container views. So, don’t hesitate to create your own container views, especially when it’s about to reuse them.

Thank you for reading! ????

Stay Up To Date

Subscribe to my newsletter and get notifiied instantly when I post something new on SerialCoder.dev.

    We respect your privacy. Unsubscribe at any time.