SwiftUI @ViewBuilder Result is a TupleView, How is Apple Using It And Able to Avoid Turning Things Into AnyVIew?

@ViewBuilder returns TupleView<T> where T is (tuple of views). I wonder what SwiftUI code looks like inside using tuple? In my code, I have to turn all the views into AnyView. I don't think Apple is doing this and throw away the type info carry inside the tuple.

How can I write my code with tuple of View and not use AnyView?

2 Likes

The internal code in SwiftUI framework may look like this:

protocol Visitor {
    func visit<T>(_ element: T)
}

public struct TupleView<T> {
    public var value: T

    @inlinable public init(_ value: T) {
        self.value = value
    }

    func accept<VisitorType: Visitor>(visitor: VisitorType) {
        // No instructions
        //  or
        // fatalError()
    }

    func accept<VisitorType: Visitor, C0, C1>(visitor: VisitorType) where T == (C0, C1) {
        visitor.visit(value.0)
        visitor.visit(value.1)
    }

    func accept<VisitorType: Visitor, C0, C1, C2>(visitor: VisitorType) where T == (C0, C1, C2) {
        visitor.visit(value.0)
        visitor.visit(value.1)
        visitor.visit(value.2)
    }

    // ...
}

Sample usage:

// Sample struct-based visitor which just prints elements
struct PrintVisitor: Visitor {
    func visit<T>(_ element: T) {
        print("element: \(element)")
    }
}

// Sample class-based visitor which aggregates some information about elements
class MaxSizeVisitor: Visitor {
    var size: CGSize = .zero

    func visit<T>(_ element: T) {
        let elementSize = size(for: element)

        size = CGSize(width: max(size.width, elementSize.width),
                      height: max(size.height, elementSize.height))
    }

    func size<T>(for element: T) -> CGSize {
        <#some code#>
    }
}

let view = TupleView((<#view0#>, <#view1#>, <#view2#>))

view.accept(visitor: PrintVisitor())

let maxSizeVisitor = MaxSizeVisitor()
view.accept(visitor: maxSizeVisitor)
print("size: \(maxSizeVisitor.size)")

How do you cast the content to a TupleView in:

init(@ViewBuilder content: @escaping () -> Content) {
    let content = content() as! TupleView((<#view0#>, <#view1#>, <#view2#>))
}

ViewBuilder is a function builder.

ViewBuilder-based closure content may produce any kind of views. If there are two or more subviews, it produces a TupleView:

HStack {
    Text("a")
    Text("b")
}

If there are no subviews, it produces an EmptyView:

HStack {
}

The best place to read about function builders is the original proposal. But there are also many articles about function builders including this answer on stackoverflow.com.

The implementation of ViewBuilder looks like this:

@_functionBuilder
public struct ViewBuilder {
    @_alwaysEmitIntoClient
    public static func buildBlock() -> EmptyView {
        .init()
    }

    @_alwaysEmitIntoClient
    public static func buildBlock<Content>(_ content: Content) -> Content where Content: View {
        content
    }

    @_alwaysEmitIntoClient
    public static func buildIf<Content>( _ content: Content?) -> Content? where Content: View {
        content
    }

    @_alwaysEmitIntoClient
    public static func buildEither<TrueContent, FalseContent>(
        first: TrueContent) -> _ConditionalContent<TrueContent, FalseContent>
        where TrueContent: View, FalseContent: View
    {
        .init(storage: .trueContent(first))
    }

    @_alwaysEmitIntoClient
    public static func buildEither<TrueContent, FalseContent>(
        second: FalseContent) -> _ConditionalContent<TrueContent, FalseContent>
        where TrueContent: View, FalseContent: View
    {
        .init(storage: .falseContent(second))
    }

    @_alwaysEmitIntoClient
    public static func buildBlock<C0, C1>(
        _ c0: C0,
        _ c1: C1) -> TupleView<(C0, C1)>
        where C0: View, C1: View
    {
        .init((c0, c1))
    }

    @_alwaysEmitIntoClient
    public static func buildBlock<C0, C1, C2>(
        _ c0: C0,
        _ c1: C1,
        _ c2: C2) -> TupleView<(C0, C1, C2)>
        where C0: View, C1: View, C2: View
    {
        .init((c0, c1, c2))
    }

    // ...
}

Sorry, my question was not clear. I'm aware of the ViewBuilder function builder. My problem is more about generics usage. I'm just trying to know if its possible to "test-cast" the result of the ViewBuilder inside the parent view.

I just want to check if content is a TupleView of 3 values, or 4, etc. But because View has associated value requirements I can't do something like this

struct ParentView<Content: View>: View {

        init(@ViewBuilder content: @escaping () -> Content) {
            if let a = content() as! TupleView<(View, View, View)> { // doesn't work
               useValue(a.value)
            }

    }

    var body: some View {
        ...
    }

    
}

For example, the TabView in SwiftUI is somewhat aware of the children because it checks their modifiers to get the tab label and image, so I was trying to replicate that.

Unfortunately, my first answer was wrong. The proper overloaded method is called when the type of the TupleView's content is known at the compile time.

When the same call to accept(visitor:) is performed from other generic method, the most generic overload is always called.

func foo<T>(_ view: TupleView<T>) {
    // Always calls `func accept<VisitorType: Visitor>(visitor: VisitorType)`
    // Nothing is printed
    view.accept(visitor: PrintVisitor())
}

Here's what the documentation says:

https://github.com/apple/swift/blob/master/docs/Generics.rst#specialization
This implementation model lends itself to optimization when we know the specific argument types that will be used when invoking the generic function. In this case, some or all of the vtables provided for the constraints will effectively be constants. By specializing the generic function (at compile-time, link-time, or (if we have a JIT) run-time) for these types, we can eliminate the cost of the virtual dispatch, inline calls when appropriate, and eliminate the overhead of the generic system. Such optimizations can be performed based on heuristics, user direction, or profile-guided optimization.

https://github.com/apple/swift/blob/master/docs/Generics.rst#overloading
Our current proposal for this is to decide statically which function is called (based on similar partial-ordering rules as used in C++), and avoid run-time overload resolution. If this proves onerous, we can revisit the decision later.

So, I've created a gist which shows how to access @ViewBuilder-provided content in a type-aware manner (i.e. access EmptyView, TupleView<T>, etc). But I haven't found a way to access the tuple items :confused:

Use Mirror/reflection? This is how I obtain the value of .tag() modifier: traverse the children members...

Edit: someone asked to see how I get .tag():

    // Do breath first search for the tag value in the view
    // Crash if cannot find any tag!!
    func getTag(_ v: Any) -> SelectionValue {
        let m = Mirror(reflecting: v)
        if let t = m.descendant("modifier", "value", "tagged") {
            return t as! SelectionValue
        } else {
            for (_, value) in m.children {
                return getTag(value)
            }
        }
        fatalError("Cannot find tag in view \(v)")
    }

where: SelectionValue: Hashable

@young I should have written "without reflection" :slightly_smiling_face:

Terms of Service

Privacy Policy

Cookie Policy