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?

3 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)")
1 Like

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))
    }

    // ...
}
2 Likes

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:

@dpereira411: Did you find a way to achieve this?

No, I gave up on using SwiftUI because I work mostly with mac apps and a lot of stuff is not possible.

I think it has to be possible in some way, because I'm pretty sure, that Apple is doing the same in sth. like List or Section...

Anyway (off-topic), I nearly thought the same as you, but it's worth staying with it... those things, that still are not possible can be achieved with NSHostingView. And I really prefer SwiftUI although its barely in alpha state ;)

I know this thread is a little old, but I found something really interesting looking through SwiftUI’s interface file.

AnyView provides a public initializer:

public struct AnyView: View {
  // ...

  public init?(_fromValue value: Any)
}

This means that SwiftUI can do something like the following in TupleView:

extension TupleView {
  /// Force-opens the given box.
  internal func open(_ box: Any) -> AnyView {
    guard let view = AnyView(_fromValue: box) else {
      fatalError("TupleView contains non-view elements") 
    }

    return view
  }

  internal var children: [AnyView] {
    switch value {
    case let tuple as? (Any, Any): return [open(tuple.0), open(tuple.1)]
    // Larger-tuple dynamic casts...
    default: return [open(value)]
    }
  }
}

Then, to access views' elements, the _VariadicView APIs can be used. You, first, need to provide a root type conforming to a certain ...Root protocol:

struct Root: _VariadicView.UnaryViewRoot {
  func body(children: _VariadicView.Children) -> some View {
    // Children is a random-access collection; here we just dump its child.
    children.forEach { dump($0) }

    return EmptyView()
  }
}

The root type is used to layout elements that are passed to it by the _VariadicView.Tree:

struct Container<Content: View>: View {
  private let tree: _VariadicView.Tree<Root, Content>

  init(@ViewBuilder _ content: () -> Content) {
    tree = _VariadicView.Tree(Root(), content: content)
  }

  var body: some View { tree }
}

VStack seems to be implemented this way. However, this approach is still quite limited by the lack of access to internal view-outputs APIs. As a result, you will only be able to use built-in containers and modifiers. Using these APIs also resulted in a lot of crashes in my test playground, so this is more of an educational post about the inner workings of SwiftUI.

4 Likes
Terms of Service

Privacy Policy

Cookie Policy