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 TupleView
s:
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:
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:
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:
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
View
likeList
? -
How can we implement a view like
Group
orTupleView
that 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:
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:
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
:
This protocol in turn looks like so:
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.Tree
takes aRoot
and the results of aViewBuilder
. -
The
Root
in turn produces aView
from a list of children. -
If the
Tree
’sContent
conforms toView
, so does theTree
itself.
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
:
For the body of this view, we’ll instantiate a _VariadicView.Tree
with a DividedVStackLayout
and our 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:
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:
Using the resulting View
feels right at home with the rest of SwiftUI:
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
.
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:
Now we can apply view modifiers to all elements of the view and still determine the layout freely: