From the documentation it looks like the signature of the tuple view builder is using parameter packs, but I can't yet actually iterate over them as far as I know. How does SwiftUI manage to use this feature already?
My understanding:
ViewBuilder transform the parameter packs into TupleView's initializer. And TupleView always only know a generics T. There is no different from the old TupleView((view1, view2)) TupleView((view1, view2, view3)) to the new TupleView((repeat each content)) on TupleView's side.
Under the hook, TupleView is probably using some dynamic feature of Swift to access such detailed info (I guess via AttributedGraph.framework or Swift.Mirror?)
FYI:
Yes, the workings of TupleView
are one of the mysteries of SwiftUI. Apple hates talking about SwiftUI internals, so I wouldn't expect an official answer to your question. But I'm pretty sure SwiftUI uses metadata to visit the elements of a TupleView
. We can do that too.
For every type in the program, Swift puts metadata in the executable. That metadata is accessible, if you understand its format, by casting the type object to a pointer. For example, unsafeBitCast(Int.self as Any.Type, to: UnsafeRawPointer.self)
gives you a pointer to the metadata for Int
. And unsafeBitCast((Int, String).self as Any.Type, to: UnsafeRawPointer.self)
gives you a pointer to the metadata for the tuple type (Int, String)
.
There is some documentation of the metadata format in the Swift repo, but it's not entirely accurate. The Azoy/Echo package is more useful for understanding and using metadata.
Anyway, for a tuple, the metadata includes the type and offset of each element of the tuple. Here's a helper struct for accessing a tuple's metadata:
struct TupleMetadata {
let raw: UnsafeRawPointer
init(_ type: Any.Type) {
raw = unsafeBitCast(type, to: UnsafeRawPointer.self)
precondition(kind == 769)
}
var kind: Int {
return raw
.load(as: Int.self)
}
var count: Int {
return raw
.advanced(by: MemoryLayout<Int>.size) // kind
.load(as: Int.self)
}
var labels: UnsafePointer<CChar>? {
return raw
.advanced(by: MemoryLayout<Int>.size) // kind
.advanced(by: MemoryLayout<Int>.size) // count
.load(as: UnsafePointer<CChar>?.self)
}
func element(atIndex i: Int) -> Element {
return Element(raw: raw
.advanced(by: MemoryLayout<Int>.size) // kind
.advanced(by: MemoryLayout<Int>.size) // count
.advanced(by: MemoryLayout<UnsafePointer<CChar>?>.size) // labels
.advanced(by: i * MemoryLayout<Any.Type>.size) // metadata
.advanced(by: i * MemoryLayout<Int>.size) // offset
)
}
struct Element {
let raw: UnsafeRawPointer
var type: Any.Type {
return raw
.load(as: Any.Type.self)
}
var offset: Int {
return raw
.advanced(by: MemoryLayout<Any.Type>.size) // metadata
.load(as: Int.self)
}
}
}
Given this, we're almost ready to visit the elements of a TupleView
. But we need one more ingredient first. With the TupleMetadata
struct above, we can get a pointer to an element of a TupleView
, and we can get the type of that element. We need a way to use the element type to load and operate on the element at the pointer. The following static
extension method on View
will do it:
extension View {
@MainActor
static func visit(_ pointer: UnsafeRawPointer) {
// Inside this method, `Self` is a concrete type conforming to `View`.
// So here we can load an instance of that concrete type and use
// its conformance requirements: the `Body` type and the `body` property.
let view = pointer.load(as: Self.self)
print("view of type \(Self.self) has Body type \(Body.self)")
if Body.self != Never.self {
print(" body = \(view.body)")
}
}
}
Finally, we can write a function that takes a TupleView
and visits each of its elements:
@MainActor
func visitElements<T>(of tupleView: TupleView<T>) {
let meta = TupleMetadata(T.self)
for i in 0 ..< meta.count {
let elementMeta = meta.element(atIndex: i)
guard let elementType = elementMeta.type as? any View.Type else {
print("element type \(elementMeta.type) at index \(i) doesn't conform to View")
continue
}
withUnsafeBytes(of: tupleView.value) { buffer in
elementType.visit(buffer.baseAddress!.advanced(by: elementMeta.offset))
}
}
}
Example use:
let tupleView = TupleView((
Text("Some text").frame(width: 100),
Circle(),
Button("Tap me", action: {})
.buttonStyle(MyButtonStyle())
))
visitElements(of: tupleView)
Output:
view of type ModifiedContent<Text, _FrameLayout> has Body type Never
view of type Circle has Body type _ShapeView<Circle, ForegroundStyle>
body = _ShapeView<Circle, ForegroundStyle>(shape: SwiftUI.Circle(), style: SwiftUI.ForegroundStyle(), fillStyle: SwiftUI.FillStyle(isEOFilled: false, isAntialiased: true))
view of type ModifiedContent<Button<Text>, ButtonStyleContainerModifier<MyButtonStyle>> has Body type Never