Secret SwiftUI: A practical use for _VariadicView
I finally found a use case for this private API
Inspiration struck. I finally realised a practical use case for _VariadicView: a reusable ChatList component that automatically applies scroll inversion on the contents.
Subscribe to Jacob’s Tech Tavern for free to get ludicrously in-depth articles on iOS, Swift, tech, & indie projects in your inbox every week.
Full subscribers unlock Quick Hacks, my advanced tips series, and enjoy my long-form articles 3 weeks before anyone else.
I’m kind of late to the party on this, frankly.
_VariadicView helps us create custom VStacks. Our own flexible tab bars. Bespoke pickers and lists. I started writing this article in March 2024, but was stumped trying to find a non-contrived example—one that would actually be useful in production.
While mulling it over on a mental background thread, this article faded into a dusty backlog alongside aborted magnum opi such as Through The Ages: Apple Concurrency APIs and Dumb Ways To Maximise Your App Binary Size.
Recently, that all changed.
At midnight, I was doing what we all do once our wives fall asleep: reading Vincent Pradeilles. Specifically, his article on building inverted scroll for messaging apps.
Inspiration struck. I finally realised a practical use case for _VariadicView: a reusable ChatList
component that automatically applies scroll inversion on the contents.
Today, much like our scrolling, let’s work backwards.
We’ll implement a scroll inversion, and experience the pain point together as our UI grows more complicated. Only then will I introduce _VariadicView, take you under the covers to explain how it all works, then demonstrate how you can use _VariadicView to address this specific pain point in your own apps.
Building an Inverted Scroll
Inverted chat scroll is the cornerstone of messaging app UX. It’s just a scroll view that starts at the bottom (with the newest messages), so the user scrolls up to see older content.
This differs from the standard scrolling feed UX, where you start at the top of the screen and scroll down for more content.
The trick to implementing this is deceptively simple: you flip the scroll view so it’s upside down. This can be done with 2 lines of SwiftUI modifiers:
.rotationEffect(.radians(Double.pi))
.scaleEffect(x: -1, y: 1, anchor: .center)
Applied to a List view, this gives us the “start at the bottom and scroll up” effect we want, however the chat message content is also inverted!
To fix this, we have to apply the same vertical inversion individually to all the pieces of content. This leaves your view looking a little like this:
struct ChatList: View {
var body: some View {
List {
ForEach(message) {
MessageView(message: $0)
.inverted()
}
}
.inverted()
}
}
This does the job, but becomes unwieldy and error-prone as you bring your chat app up to parity with iMessage.
As more types of view are implemented like messages, date headers, and text fields, this gets messy fast.
List {
TextField()
.inverted()
TextEntrySeparator()
.inverted()
ForEach(message) {
if message.isDateHeader {
MessageDateHeader()
.inverted()
}
MessageView(message: $0)
.inverted()
}
}
.inverted()
Enter: _VariadicView.
WTF is a _VariadicView?
_VariadicView is a private-ish SwiftUI API, and the underlying implementation detail for many of the container views you use every day.
It sounds pretty obscure, however its naming is actually pretty straightforward: it’s a view that can be passed any number of views, hence variadic.
_VariadicView lets you do one thing well:
Create reusable container components
…that can handle multiple types of child view passed in
…to which you can apply similar sets of modifiers.
Despite being an underscored “private” API, it’s safe to use in production—many @frozen
SwiftUI APIs explicitly conform to the protocol. You won’t have to worry about app review when using it—the API is already emitted into your code implicitly whenever you use a SwiftUI container view like HStack.
How to use _VariadicView
We want to set up a simple container for all our chat content that handles the inversion automatically. We want to implement this once, then apply the inverted scrolling for free anytime we like.
_VariadicView.Tree
We start by creating this container view: ChatList.
struct ChatList<Content: View>: View {
var content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
_VariadicView.Tree(ChatLayout()) {
content
}
}
}
We initialise this view with content—the child views, such as messages, passed into the ChatList container.
The body declares a _VariadicView.Tree. This represents a SwiftUI view tree which can be rendered by the layout engine. The API declaration looks like this:
public enum _VariadicView {
public struct Tree<Root, Content> where Root : _VariadicView_Root
}
We pass in a Layout that conforms to _VariadicView_MultiViewRoot and content—the child views passed into our container.
_VariadicView_MultiViewRoot
Now, we can implement this Layout. This is where the magic happens.
struct ChatLayout: _VariadicView_MultiViewRoot {
func body(children: _VariadicView.Children) -> some View {
List {
ForEach(children) { child in
child
.inverted()
.listRowSeparator(.hidden)
}
}
.listStyle(.plain)
.inverted()
}
}
The body of a _VariadicView_MultiViewRoot is the root of the SwiftUI view tree hierarchy. It lays out the child views that represent the leaf nodes in this view hierarchy.
This gives us a new power not present in vanilla SwiftUI code: we can declare a container view with child views, and perform modifications to, or between, each child view without the parent knowing.
In short:
The power of _VariadicView is the ability to do stuff between the root and the leaf nodes of a container view.
This means our container can wrap functionality like inverting a scroll, and re-inverting each child view individually. Instead of leaking this complexity into the top-level view every time we touch the list, this inversion becomes an implementation detail—we can hide this complexity in the container. The devs adding new chat functionality or polishing the UI may never know about it.
_VariadicView.Children
Inside the body, we have some pretty standard SwiftUI code to represent a scrolling list, set basic styling, and vertically invert all the content.
_VariadicView.Children represents the content in our ChatList—the child views passed into our container. This conforms to RandomAccessCollection, Identifiable, and View. This means we can iterate through and render each child, even subscripting them if we want further individual customisation.
Putting It All Together
Now that we’ve created our ChatList container, let’s see it in action.
NavigationView {
ChatList {
TextField("Reply", text: $reply)
ForEach(messages) {
ChatMessageView(message: $0)
}
}
.navigationTitle("Chat")
}
ChatList internally handles the UI inversion, so we can focus fully on building a sick messaging app for our users!
Despite being the best example I could find in 11 months, this approach isn’t limited to Chat UIs. Any dynamic container, carousel, or list can apply _VariadicView to create flexible components to delight your colleagues and end-users.
If you’re still confused about _VariadicView, that’s because it’s confusing. The key takeaway is that _VariadicView allows you to do create containers that do stuff between their root and leaf nodes.
In practical, concrete, non-jargony terms, _VariadicView is occasionally quite useful when you want to wrap some annoying domain-specific boilerplate in a component.
If you want to check out the whole project, goto _Secret_SwiftUI on Github.
The open-source project also includes _trait to create a custom Tab Bar. _trait is another private SwiftUI API that allows you to associate any type of metadata to a view, which can then be queried within the container. This data ostensibly communicates up the view tree during the initial layout pass, making _traits more performant than preference keys.
I didn't write much about it here, because I still haven’t thought of a good use case. Vincent, if you can think of one, consider it a free idea!
Thanks for reading Jacob’s Tech Tavern! 🍺
Your link to the Github repo at the end doesn't seem to be working!
Nice use case and one I may adopt 😅