Streaming is available in most browsers,
and in the Developer app.
-
Demystify SwiftUI containers
Learn about the capabilities of SwiftUI container views and build a mental model for how subviews are managed by their containers. Leverage new APIs to build your own custom containers, create modifiers to customize container content, and give your containers that extra polish that helps your apps stand out.
Chapters
- 0:00 - Introduction
- 3:17 - Composition
- 10:42 - Sections
- 13:18 - Customization
- 16:52 - Next steps
Resources
Related Videos
WWDC23
WWDC21
-
Download
Hello, my name is Matt, and I work on SwiftUI. This video is all about building custom container views in SwiftUI. SwiftUI provides many kinds of full-featured containers in its API, like the List container view. Container views use a trailing view builder closure, to wrap their content. View builders allow content to be defined statically, like this hard-coded list of Text views. But can also define content dynamically, such as using a ForEach view to generate Text views from data and view builders support composing any kind of content together, within the same container. Some containers also support more advanced capabilities, such as grouping content into distinct sections with configurable headers and footers. And container-specific modifiers for customizing content. Like in this example, where I’m hiding the separators that a List normally draws between its rows. In this video, I’ll show how to use several new APIs to build custom container views that can support all of these capabilities, and more. I’ll start by explaining how to make custom containers support any composition of content, maximizing their flexibility.
I’ll also demonstrate how to add support for sections. And later, I’ll describe how to define custom modifiers for decorating content within a container.
For each topic, I’ll also discuss some of the core concepts behind SwiftUI and its API design. So I’ll jump right in and start...
What’s this? Oh wow, Sommer and Sam from the "What’s New in SwiftUI" talk are throwing a karaoke party to celebrate WWDC! To RSVP, I have to submit the song that I’m planning to sing, but I have no idea what song to choose! In times of crisis like this, I’m going to do what I always do. Solve my problems using the power and flexibility of SwiftUI’s declarative API! So in this video, I’ll also pick the perfect karaoke song, and I’ve been working on something that I think might help. My trusty display board! I’ve already started brainstorming a few possible karaoke songs. I’m using an initializer, that maps my collection of song ideas into Text views, which are written on cards that get pinned to the board. In the DisplayBoard’s implementation I’m using a custom layout, to randomly position the cards across the board. And the cards themselves are constructed using a ForEach view, which handles iterating over the input data. Generating a content view from each data element and wrapping it in a custom CardView that I made.
This is a great start, but my DisplayBoard container is limiting my creativity, only allowing cards to be constructed from a single collection of data. I can make my container more flexible by adding support, for more kinds of composition in its content. But first, it’s important to understand what composition means. Consider a SwiftUI List like this one, displaying a set of song ideas that Sam recommended to me. The List is initialized with a collection of data, just like my DisplayBoard. But SwiftUI supports creating Lists in other ways too.
For example, I can make a List by manually writing out a set of views instead, like I did for this list of my own song ideas.
SwiftUI bridges the gap between these two techniques, by offering APIs for composing different kinds of content together.
For example, I can rewrite the data-driven list using a ForEach view. This supports the same functionality as before, but the ForEach view can be nested within the view builder.
And that’s important, because by defining the content for both Lists using only views. That means I can combine them together into a single, unified List, displaying all of the song ideas I’ve collected so far. This unified List is an example of composition. I can define the first three rows statically, using hard-coded Text views, while also generating the rest of the rows dynamically, using data, all within the same List.
I want to support flexible composition in my DisplayBoard container as well. To do that, I’ll need to change my implementation. The first step is to refactor my container, so it can be initialized using only a view builder. I’ll start by replacing my existing, data-driven properties with a single, generic view property instead.
By adding the ViewBuilder attribute, my default initializer will automatically construct the content using a trailing view builder closure. Next, I need to update my view body to use the new content view. I can do this with the help of a new API, called ForEach(subviewOf:) This new ForEach initializer accepts a single view value as input. And passes back each of its subviews into the trailing view builder, allowing them to be transformed into a different kind of view, like my card views. With this new implementation, I can now take my List of song ideas from earlier.
And wrap the same content in my DisplayBoard instead, transforming each Text view into a card on the board.
This is a big improvement, but it’s important to understand how this works. Back in my implementation, I’ll drill in on the new API. ForEach(subviewOf: What, exactly, is a subview? A subview is simply a view that’s contained within another view. Inspecting the content, how many subviews are there? Well, like the answer to all great questions, it depends! Just considering the top-level views in the code, there are four. The three Text views, and the ForEach view. But the ForEach is more than just one view, it represents a collection of views generated from data.
And in this case, that resolves to nine subviews, one for each of Sam’s favorite songs. Meaning this DisplayBoard’s content, actually resolves to a total of twelve distinct subviews. Which is evident in the twelve cards displayed on the board.
And is also consistent with the same content in a List, displaying twelve individual rows.
It’s important to understand the distinction, between these two different kinds of subviews.
The four subviews from the DisplayBoard’s code, highlighted in orange, are known as declared subviews. Whereas the views that will appear onscreen, highlighted in blue, are known as the resolved subviews. These include the three Text views I defined manually, as well as the nine Text Views generated by the ForEach.
In SwiftUI’s declarative system, declared subviews define a recipe for producing resolved subviews while a SwiftUI app is running.
For example, a ForEach view is a declared subview that has no specific visual appearance or behavior by itself. Instead, the entire purpose of a ForEach view is to produce a collection of resolved subviews.
A Group view is another example of a built-in container, and represents a collection of resolved subviews. For example, a Group of three Text views will resolve to exactly three corresponding subviews.
It’s even possible for some declared subviews, to produce no resolved subviews at all, like an EmptyView. Or to conditionally resolve to a different number of subviews, such as the different branches of an if statement.
The new ForEach(subviewOf:) API, iterates over only the resolved subviews of the content. This makes it possible for my container to support any possible composition of content with even less code, because SwiftUI will do the work of resolving the subviews for me, no matter how those subviews are declared in code.
Supporting flexible composition, makes adding new songs to my board incredibly easy. In addition to Sam’s songs, Sommer was also kind enough to recommend a few of her favorite songs, which I can add using another ForEach view. And this is possible without requiring any additional changes in my container’s implementation. However, it’s gotten so easy to add new ideas, that now it’s getting overwhelming to see all the cards! To fix this, I’m going to scale down the size of the cards when the board gets too crowded. I want to scale down the size of the cards if more than 15 of them get added to the board. To count the number of cards, I can use another new API. It’s called Group(subviewsOf:), which I can wrap around the ForEach in my implementation. Like the previous ForEach(subviewOf:) API, this view accepts a view as input and resolves its subviews.
But instead of iterating over them one at a time, the Group(subviewsOf:) API passes back a collection, of all of the resolved subviews.
I can use the count property on the collection to check the total number of cards, configuring my CardViews to use a smaller scale, when there are more than 15 of them.
When I rerun my app, the smaller size keeps the cards from overlapping too much. That’s helpful for reading the cards, but my board still feels a bit disorganized. So next, I’ll clean things up by adding support for sections.
A List is an example of a built-in container that supports sections, using SwiftUI’s Section view. A Section view behaves much like a Group view, but with extra section-specific metadata, such as optional headers and footers. For my display board, my goal is to create a separate section for each person's favorite songs. However, custom containers don’t support sections by default, so I’ll have to do some extra work to enable them.
Here’s a rough sketch of the design I have in mind, dividing the board into vertical columns for each section, with their headers appearing at the top.
In my implementation, I’ll start by factoring out my existing card layout code into its own view.
I’ll reuse this view when laying out the cards within each individual section.
Next, I’ll wrap the section content in a horizontal stack, for dividing the board into columns. To construct the columns, I’ll need to access the information for any Section views that exist within my display board’s content. For that, I can use another new API on ForEach.
It's called ForEach(sectionOf:). And this works similarly to ForEach(subviewOf:), taking in a view instance as its input.
But this version iterates over each section it detects within the view, vending a section configuration into its view builder. Each section has a property for its content view, which I can pass to the helper view I created earlier for laying out the cards.
And for some extra polish, I’ll add a custom background to each section, to help visually distinguish them from each other.
Running the app again, I can tell that the cards appear a little more organized than before, with each section laid out in its own column. Now I’ll add support for displaying the section headers.
I’ll start by wrapping each section in a VStack, to hold both the header and the content.
Next, I’ll use an if statement to check if the section has a header, using the isEmpty property, which returns whether or not the header contains any resolved subviews.
If the header does exist, I’ll display it within a custom header card that I wrote earlier.
Checking out the board, there’s now a distinct, visible header card placed above each section.
But to make progress on choosing a song, I need to start crossing things off. And I can enable that by adding support, for customizing the content of my container. At the beginning of the video, I showed an example using the .listRowSeparator() modifier. Even though this modifier is applied to views within the List, the List itself is responsible for implementing its behavior when deciding to draw the separators in between the rows.
In my display board, I’d like to support modifying the cards to cross them off if I decide to not choose that specific song. There’s a new API for building these kinds of container-specific modifiers, called container values. Container values are a new kind of keyed storage, similar to concepts like the Environment and Preferences.
But unlike environment values, which are passed down the entire view hierarchy. Or Preferences, which pass values up the entire view hierarchy to every containing view. The container values of a resolved subview can only be accessed by their direct container, making them the perfect tool for implementing container-specific customization options.
In my display board, I’m going to use container values to create a custom view modifier for crossing off cards. Defining a new kind of container value only requires a few lines of code.
First, I’ll create an extension on the ContainerValues type, which is a new type in SwiftUI.
Within my extension, I’m going to declare a property using the new Entry macro, storing a boolean value to track if a card has been rejected.
The Entry macro is a new API, that provides a convenient syntax for adding new values to SwiftUI keyed storage types, including environment values, focus values, and more.
Next, I’ll declare a custom view modifier as a convenience for setting my property, which calls through to the new containerValue() API modifier, passing the property’s key path and the new value to set.
Now I’ll add support for my new container value, within my container. In my section implementation, I’ll need to customize each card view depending on whether or not its content has been rejected. I can do that using the new containerValues property. Container values can be read from both resolved subviews and sections.
I’ll pass my custom value to the isRejected parameter on the CardView, which will display a custom decoration if the card has been rejected. And with my new modifier, I can now start crossing off some songs! First, I love the song Scrolling in the Deep, but I’m not sure I have the vocal range to pull that off at karaoke.
So I’ll cross it off on the board, which gets rendered with a big, red slash.
Sam also called dibs on a few of his songs, so I’ll reject those too.
I’m not sure what Sommer plans to sing, so I’ll reject every song in her section, just to be safe. Applying my modifier to an entire section will set the value on all of its subviews.
Meaning all of Sommer’s songs on the right have been crossed off. Alright, I’ve made a lot of progress towards finding the perfect karaoke song, but I still have to make a final decision. While I give that some more thought, I encourage you to try out these new APIs. Use the new initializers on ForEach and Group, to iterate over, and transform, the resolved subviews and sections of a view. Add support for sections, if your custom container’s design can support them. But if sections don’t make sense in your container, that’s okay, adding section support is optional. Lastly, use container values for customizing, and decorating, individual pieces of content.
With the help of these new APIs, I’ve narrowed down my options to just a handful of remaining songs. But I’m now realizing, there’s one more song I haven’t considered yet and I think it might be a winner.
“I Will Always Subview” by Whitney View-ston.
The only thing left to do now, is send my RSVP back to Sommer and Sam, and finish this video, because I really have to start rehearsing my song! Until next time!
-
-
0:20 - SwiftUI Lists
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") }
-
0:36 - SwiftUI Lists
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(otherSongs) { song in Text(song.title) } }
-
0:54 - SwiftUI Lists
List { Section("Favorite Songs") { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } Section("Other Songs") { ForEach(otherSongs) { song in Text(song.title) } } }
-
1:00 - SwiftUI Lists
List { Section("Favorite Songs") { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } Section("Other Songs") { ForEach(otherSongs) { song in Text(song.title) .listRowSeparator(.hidden) } } }
-
2:35 - Data-driven DisplayBoard
@State private var songs: [Song] = [ Song("Scrolling in the Deep"), Song("Born to Build & Run"), Song("Some Body Like View"), ] var body: some View { DisplayBoard(songs) { song in Text(song.title) } }
-
2:47 - DisplayBoard implementation
// Insert code snvar data: Data @ViewBuilder var content: (Data.Element) -> Content var body: some View { DisplayBoardCardLayout { ForEach(data) { item in CardView { content(item) } } } .background { BoardBackgroundView() } }
-
3:08 - Data-driven DisplayBoard
@State private var songs: [Song] = [ Song("Scrolling in the Deep"), Song("Born to Build & Run"), Song("Some Body Like View"), ] var body: some View { DisplayBoard(songs) { song in Text(song.title) } }
-
3:30 - List composition
List(songsFromSam) { song in Text(song.title) }
-
3:46 - List composition
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") }
-
3:56 - List composition
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } List(songsFromSam) { song in Text(song.title) }
-
4:05 - List composition
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } List { ForEach(songsFromSam) { song in Text(song.title) } }
-
4:24 - List composition
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
4:59 - DisplayBoard implementation
var data: Data @ViewBuilder var content: (Data.Element) -> Content var body: some View { DisplayBoardCardLayout { ForEach(data) { item in CardView { content(item) } } } .background { BoardBackgroundView() } }
-
5:15 - DisplayBoard implementation
// DisplayBoard implementation @ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { ForEach(data) { item in CardView { content(item) } } } .background { BoardBackgroundView() } } DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } DisplayBoard { ForEach(songsFromSam) { song in Text(song.title) } }
-
5:27 - DisplayBoard implementation
@ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { ForEach(subviewOf: content) { subview in CardView { subview } } } .background { BoardBackgroundView() } }
-
5:52 - List composition
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
5:57 - DisplayBoard composition
DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
6:12 - DisplayBoard implementation
@ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { ForEach(subviewOf: content) { subview in CardView { subview } } } .background { BoardBackgroundView() } }
-
6:23 - DisplayBoard subviews
DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
6:36 - Declared vs. resolved views
DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } } // 3 resolved subviews Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") // 9 resolved subviews Text("I Container Multitudes") … Text("Love Stack")
-
7:11 - List subviews
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
7:19 - Declared vs. resolved views
DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } } // 3 resolved subviews Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") // 9 resolved subviews Text("I Container Multitudes") … Text("Love Stack")
-
8:00 - Resolved ForEach
// 1 declared view ForEach(songsFromSam) { song in Text(song.title) } // 9 resolved subviews Text("I Container Multitudes") … Text("Love Stack")
-
8:16 - Resolved Group
// 1 declared view Group { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } // 3 resolved subviews Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View")
-
8:32 - Resolved EmptyView
// 1 declared view EmptyView() // Zero resolved subviews
-
8:39 - Resolved if expression
// Insert code snippet.
-
8:48 - DisplayBoard implementation
@ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { ForEach(subviewOf: content) { subview in CardView { subview } } } .background { BoardBackgroundView() } }
-
9:11 - DisplayBoard composition
DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
9:17 - DisplayBoard composition
DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } ForEach(songsFromSommer) { song in Text(song.title) } }
-
9:44 - DisplayBoard implementation
@ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { ForEach(subviewOf: content) { subview in CardView { subview } } } .background { BoardBackgroundView() } }
-
9:55 - DisplayBoard implementation
@ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { Group(subviewsOf: content) { subviews in ForEach(subviews) { subview in CardView { subview } } } } .background { BoardBackgroundView() } }
-
10:19 - DisplayBoard implementation
@ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { Group(subviewsOf: content) { subviews in ForEach(subviews) { subview in CardView( scale: subviews.count > 15 ? .small : .normal ) { subview } } } } .background { BoardBackgroundView() } }
-
10:47 - List sections
List { Section("Favorite Songs") { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } Section("Other Songs") { ForEach(otherSongs) { song in Text(song.title) } } }
-
11:03 - DisplayBoard sections
DisplayBoard { Section("Matt's Favorites") { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } Section("Sam's Favorites") { ForEach(songsFromSam) { song in Text(song.title) } } Section("Sommer's Favorites") { ForEach(songsFromSommer) { song in Text(song.title) } } }
-
11:26 - Implementing DisplayBoard sections
DisplayBoard sections @ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { Group(subviewsOf: content) { subviews in ForEach(subviews) { subview in CardView( scale: subviews.count > 15 ? .small : .normal ) { subview } } } } .background { BoardBackgroundView() } }
-
11:35 - Implementing DisplayBoard sections
@ViewBuilder var content: Content var body: some View { DisplayBoardSectionContent { content } .background { BoardBackgroundView() } } struct DisplayBoardSectionContent<Content: View>: View { @ViewBuilder var content: Content ... }
-
11:42 - Implementing DisplayBoard sections
@ViewBuilder var content: Content var body: some View { HStack(spacing: 80) { ForEach(sectionOf: content) { section in DisplayBoardSectionContent { section.content } } } .background { BoardBackgroundView() } }
-
12:48 - Implementing DisplayBoard section headers
@ViewBuilder var content: Content var body: some View { HStack(spacing: 80) { ForEach(sectionOf: content) { section in VStack(spacing: 20) { if !section.header.isEmpty { DisplayBoardSectionHeaderCard { section.header } } DisplayBoardSectionContent { section.content } .background { BoardSectionBackgroundView() } } } } .background { BoardBackgroundView() } }
-
13:30 - List customization
List { Section("Favorite Songs") { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } Section("Other Songs") { ForEach(otherSongs) { song in Text(song.title) .listRowSeparator(.hidden) } } }
-
14:46 - Custom container values
extension ContainerValues { @Entry var isDisplayBoardCardRejected: Bool = false } extension View { func displayBoardCardRejected(_ isRejected: Bool) -> some View { containerValue(\.isDisplayBoardCardRejected, isRejected) } }
-
15:42 - Implementing DisplayBoard customization
struct DisplayBoardSectionContent<Content: View>: View { @ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { Group(subviewsOf: content) { subviews in ForEach(subviews) { subview in let values = subview.containerValues CardView( scale: (subviews.count > 15) ? .small : .normal, isRejected: values.isDisplayBoardCardRejected ) { subview } } } } } }
-
16:15 - DisplayBoard customization
DisplayBoard { Section("Matt's Favorites") { Text("Scrolling in the Deep") .displayBoardCardRejected(true) Text("Born to Build & Run") Text("Some Body Like View") } Section("Sam's Favorites") { ForEach(songsFromSam) { song in Text(song.title) .displayBoardCardRejected(song.samHasDibs) } } Section("Sommer's Favorites") { ForEach(songsFromSommer) { Text($0.title) }}} } .displayBoardCardRejected(true) }
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.