SwiftUI under the Hood: Variadic Views
Matching SwiftUI’s view APIs in their ergonomics is hard to get right. In this post we’ll learn how to write view APIs that feel truly native to the platform.
SwiftUI is a highly expressive framework for writing UI code. Using the ViewBuilder result builder, we simply write out the views in a declarative manner one after another and have the result builder take care of creating the necessary TupleViews:
let stack: VStack<TupleView<(Text, Text)>> = VStack {
Text("First")
Text("Second")
}
It just works™, as they say. But zooming in on TupleView, we notice something interesting: it is transparent to view modifiers!
Let’s manually create a TupleView and invoke a view modifier on it:
let tupleView = ViewBuilder.buildBlock(
Text("First"), Text("Second")
)
tupleView.border(.blue)
Somebody new to SwiftUI might expect the blue border to be drawn around the combined shape of the two views, but this isn’t the case. Instead, each member of the tuple receives its own border.
We can nest tuple views and observe that this behavior composes:
// TupleView<(Text, Text)>
let inner = ViewBuilder.buildBlock(
Text("First"), Text("Second")
)
// TupleView<(TupleView<(Text, Text)>, Text)>
let outer = ViewBuilder.buildBlock(
inner, Text("Third")
)
outer.border(.blue)
Even more curious, if we wrap this nested tuple in a List, we can see that the resulting view is able to inject list row separators between each individual view:
let inner = ViewBuilder.buildBlock(
Text("First"), Text("Second")
)
let outer = ViewBuilder.buildBlock(
inner, Text("Third")
)
List {
outer
}
The same is true if we use the more common Group. However, if we were to use e.g. VStack to wrap the three Text views, the result is only a single cell.
So evidently, there are views that can be “disassembled” like TupleView and Group and those that present themselves as a single element to their parent, like VStack.
This poses two questions:
-
How can we modify the individual children of a
ViewlikeList? -
How can we implement a view like
GrouporTupleViewthat is open to these adjustments?
Spelunking through SwiftUI, you might come across some interesting bits of underscored API. For example, the _printChanges helper can be extremely valuable when debugging your code, but there is more!
If we take a look at the interface presented by VStack, some interesting bits stand out:
@frozen
public struct VStack<Content> : View where Content : View {
@usableFromInline
internal var _tree: _VariadicView.Tree<_VStackLayout, Content>
@inlinable
public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content) {
_tree = .init(
root: _VStackLayout(alignment: alignment, spacing: spacing), content: content())
}
public typealias Body = Swift.Never
}
Because its initializer is @inlinable, it is effectively available as source code. The @inlinable attribute allows the compiler to replace calls in your code with the body of the method, in this case creating a _VariadicView.Tree that receives a _VStackLayout as well as the Content the VStack is parameterized with.
_VStackLayout doesn’t make a big secret about its functionality: it’s the brains of VStack. Given how big of a share stack views are likely going to have in an app, enabling this optimization makes sense. Since VStack is able to layout its children and access their layoutPriority, it requires the ability to address them individually.
Let’s take a look at _VariadicView.Tree next. Again, we notice @inlinable optimizations at work:
public enum _VariadicView {
@frozen
public struct Tree<Root, Content> where Root : _VariadicView_Root {
public var root: Root
public var content: Content
@inlinable
internal init(root: Root, content: Content) {
self.root = root
self.content = content
}
@inlinable
public init(_ root: Root, @ViewBuilder content: () -> Content) {
self.root = root
self.content = content()
}
}
}
The _VariadicView.Tree is parameterized by a type conforming to _VariadicView_Root as well as Content.
We’ve already seen that Content may conform to View in the previous example and in fact, if Content does, so does _VariadictView.Tree:
extension _VariadicView.Tree : View where Root : _VariadicView_ViewRoot, Content : View {}
This protocol in turn looks like so:
public protocol _VariadicView_ViewRoot : _VariadicView_Root {
associatedtype Body : SwiftUI.View
@ViewBuilder
func body(children: _VariadicView.Children) -> Body
}
If you squint a little, this looks a lot like ButtonStyle and similar protocols. And _VariadicView.Children sounds like it might be a collection of the view’s children?
Let’s do a quick recap:
-
A
_VariadicView.Treetakes aRootand the results of aViewBuilder. -
The
Rootin turn produces aViewfrom a list of children. -
If the
Tree’sContentconforms toView, so does theTreeitself.
Now that we know just enough to be dangerous, we can get our hands dirty and see how far get.
Writing our own container view
Let’s try writing our own container view in the vein of List and VStack that inserts a Divider between every view.
First we need to set up an initializer that takes a @ViewBuilder:
struct DividedVStack<Content: View>: View {
var content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
}
For the body of this view, we’ll instantiate a _VariadicView.Tree with a DividedVStackLayout and our content:
struct DividedVStack<Content: View>: View {
var body: some View {
_VariadicView.Tree(DividedVStackLayout()) {
content
}
}
}
For the DividedVStackLayout, we’ll follow _VStackLayout’s lead and conform to _VariadicView_UnaryViewRoot.
We’ll also have to implement body(children:). Because _VariadicView.Children conforms to View itself, the following already compiles:
struct DividedVStackLayout: _VariadicView_UnaryViewRoot {
@ViewBuilder
func body(children: _VariadicView.Children) -> some View {
children
}
}
However, it doesn’t stop there. _VariadicView.Children is also a RandomAccessCollection and its Element conforms both to View as well as Identifiable.
This makes _VariadicView.Children an ideal candidate for use with a ForEach. We add a Divider after every element while we’re at it. For layout, we can rely on VStack.
Finally, we want to omit the last Divider. To do that, we can make a note of the last id in the collection and skip the Divider once we’ve reached it:
struct DividedVStackLayout: _VariadicView_UnaryViewRoot {
@ViewBuilder
func body(children: _VariadicView.Children) -> some View {
let last = children.last?.id
VStack {
ForEach(children) { child in
child
if child.id != last {
Divider()
}
}
}
}
}
Using the resulting View feels right at home with the rest of SwiftUI:
DividedVStack {
Text("First")
Text("Second")
Text("Third")
}
We also wanted to build a view that would be transparent to view modifiers or container views. If we try this with our DividedVStack, we notice it behaves like a single view – much like VStack.
DividedVStack {
Text("First")
Text("Second")
Text("Third")
}
.border(.blue)
This is where the Unary in _VariadicView_UnaryViewRoot comes into play. Trees whose Root conforms to this protocol act like a single view rather than a list of views. Luckily, the equivalent _VariadicView_MultiViewRoot protocol exists as well.
Trees whose Root conforms to _VariadicView_MultiViewRoot aren’t limited in this way. Armed with this protocol, we can separate the layout-aspect of our DividedVStack from injecting the dividers:
struct Divided<Content: View>: View {
/* … */
var body: some View {
_VariadicView.Tree(DividedLayout()) { content }
}
}
struct DividedLayout: _VariadicView_MultiViewRoot {
@ViewBuilder
func body(children: _VariadicView.Children) -> some View {
let last = children.last?.id
ForEach(children) { child in
child
if child.id != last {
Divider()
}
}
}
}
Now we can apply view modifiers to all elements of the view and still determine the layout freely:
HStack {
Divided {
Text("First")
Text("Second")
Text("Third")
}
.border(.blue)
}