SwiftUI has an undocumented system for interacting with collections of View
types known as VariadicView
. The enum _VariadicView
is the entry point to this
system, which includes other types like _VariadicView_MultiViewRoot
and _VariadicView.Tree
. The details of these were explored in a great post from MovingParts and
there have been a few other helpful blogs about it.
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public enum _VariadicView {
public typealias Root = SwiftUI._VariadicView_Root
public typealias ViewRoot = SwiftUI._VariadicView_ViewRoot
public typealias Children = SwiftUI._VariadicView_Children
public typealias UnaryViewRoot = SwiftUI._VariadicView_UnaryViewRoot
public typealias MultiViewRoot = SwiftUI._VariadicView_MultiViewRoot
@frozen public struct Tree<Root, Content> where Root : SwiftUI._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, @SwiftUI.ViewBuilder content: () -> Content) {
self.root = root
self.content = content()
}
}
}
_VariadicView
in SwiftUI’s public interfaceWhen I first read about it, I didn’t see the applications to my code. As with most SwiftUI, it relies heavily on generics and can be difficult to see how to use it just from reading the API. Since then, I’ve made it a core part of SnapshotPreviews and learned that, despite being a private API, it is very safe to use in production - in fact, many popular apps use it extensively.
This post will explain the specific use case I found for extracting snapshots from SwiftUI previews. Hopefully a concrete example will inspire others to use this powerful SwiftUI feature!
Snapshot previews
SnapshotPreviews is an open-source framework to turn Xcode previews into image snapshots. It powers the Emerge Tools Snapshots product that brings automated snapshot testing to pull requests.
When you create an Xcode preview with SwiftUI's PreviewProvider
, you can return multiple previews in a single provider using containers such as Group
, TupleView
, and ForEach
. For example,
this code would create two previews:
struct Provider: PreviewProvider {
static var previews: some View {
Group {
Text("Hello")
Text("World")
}.previewLayout(.sizeThatFits)
}
}
To snapshot each of these previews, we need to take the View
from a preview provider and split it into multiple views, which can be rendered separately. First,
I tried doing this using runtime reflection.
Inspecting Swift
The open-source framework ViewInspector uses runtime reflection like Mirror
to traverse
a SwiftUI view hierarchy and access subviews. You can use it to access internal properties of Apple views, like each subview of a TupleView
.
However, this solution required brittle checks for every type of view and was prone to breaking,
Ex: Optional, Group, Tuple, and ForEach.
Additionally, it wouldn’t work if you had a custom View that conformed to VariadicView or a custom ViewModifier
that changes the number of views. I needed something more robust to get snapshots working well.
Layouts
In iOS 16, SwiftUI added native support for layouts. This lets you treat a View
as a collection of views
to arrange in custom positions. It does almost what I needed, but it does not support hiding views in the collection.
Variadic View
The solution to match Xcode’s previewing behavior is to use VariadicView. The whole thing is about 10 lines of code.
First, I define a new type, ViewSelector
, which conforms to _VariadicView_MultiViewRoot
and, therefore, also View
. This type has one property position
, which selects
the position in the child views to display.
fileprivate struct ViewSelector: _VariadicView_MultiViewRoot {
let position: Int
func body(children: _VariadicView.Children) -> some View {
children[position]
}
}
The body function, required by the protocol, receives the children as _VariadicView.Children
. This type conforms to RandomAccessCollection
so we
can subscript the children and access only the selected position.
Next, an extension on View
provides functionality to select the subview at a specific index. This
uses _VaradicView.Tree
. The new view type is the tree’s root and self
is the content of the tree.
extension View {
func selectSubview(_ position: Int) -> some View {
_VariadicView.Tree(ViewSelector(position: position)) {
self
}
}
}
Is it safe to use?
We can see all the details of this API in /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/arm64-apple-ios.swiftinterface
.
But, this is a private API, so is it safe to use in production? I think it is, and it has been used successfully by many apps.
The key reason you can use it is the VariadicView API is already used in apps, even without developers writing the code explicitly. _VariadicView
can be emitted
into your app just by using HStack
since it is part of the @inlinable
initializer:
@frozen public struct HStack<Content> : SwiftUI.View where Content : SwiftUI.View {
@usableFromInline
internal var _tree: SwiftUI._VariadicView.Tree<SwiftUI._HStackLayout, Content>
@inlinable public init(alignment: SwiftUI.VerticalAlignment = .center, spacing: CoreFoundation.CGFloat? = nil, @SwiftUI.ViewBuilder content: () -> Content) {
_tree = .init(
root: _HStackLayout(alignment: alignment, spacing: spacing), content: content())
}
The struct is also marked with @frozen
, making _VariadicView.Tree
part of the SwiftUI ABI. If it changed, it would break previously compiled apps that used HStack
.
There are also public SwiftUI types that conform to _VariadicView.UnaryViewRoot
, such as GridLayout
,
making it impossible to remove these protocols. Thanks to @EricHoracek for pointing out these examples!
Since this VariadicView code is already used by having the compiler emit it inline, it’s unlikely there would be any problem using it from your source code directly. Emerge is now using it as the root view of every snapshot we generate, and it might be useful for your views as well!
Share on Twitter