Search code examples
macosswiftuiviewbuilder

How wrong type error can be solved in @viewbuilder


I'm trying to combine the View1 and View2 using a @viewBuilder named SeparateViewPanels. But it throws the following error. Why is that? I can see that the error is about a wrong type I used. As I see it basically says I can't use the type of view when it expects a type of content.

Error: Cannot convert return expression of type 'TupleView<(View1, View2)>' to return type '(toolBar: Content, view2D: Content)'

but even if I replace the content with anyView etc, it still throws more errors. So it was not the solution. How to solve this? I'm trying out the @viewbuilder functionality so I can't completely eliminate @viewbuilder

import AppKit
import Combine
import SwiftUI

@main
struct demo44App: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

 

 

struct View1: View {
    var body: some View {
        Text("Hello, World!")
    }
}

struct View2: View {
    var body: some View {
        Text("Hello, World!")
    }
}

struct ContentView: View {
    var body: some View {
        SeparateViewPanels {
            View1()
            View2()
        }
    }
}

struct SeparateViewPanels<Content: View>: View {
    @State var propertiesWindowWidth: Double = 100
    let contents: () -> (toolBar: Content, view2D: Content)

    init(@ViewBuilder contents: @escaping () -> (toolBar: Content, view2D: Content)) {
        self.contents = contents
    }

    var body: some View {
        GeometryReader { geo in
            VStack(alignment: .center) {
                HStack (alignment: .center, spacing: 0) {
                    Rectangle()
                        .fill(.blue)
                        .opacity(0.7)
                        .frame(width: 100, height: geo.size.height)
                        .overlay {
                            contents().toolBar
                        }
                    Rectangle()
                        .fill(.gray)
                        .opacity(0.7)
                        .frame(width: 100, height: geo.size.height)
                        .overlay {
                            contents().view2D
                        }
                }
            }
        }
    }
}

Solution

  • toolBar and view2D are views of different types, so SeparateViewPanels should have 2 type parameters - Toolbar and View2D. Then, change the return type of the content closure to TupleView<(Toolbar, View2D)> as the error message suggests.

    You can get the separate views from a TupleView by using the value.0 and view.1 properties.

    struct SeparateViewPanels<ToolBar: View, View2D: View>: View {
        @State var propertiesWindowWidth: Double = 100
        let contents: () -> TupleView<(ToolBar, View2D)>
    
        init(@ViewBuilder contents: @escaping () -> TupleView<(ToolBar, View2D)>) {
            self.contents = contents
        }
    
        var body: some View {
            GeometryReader { geo in
                VStack(alignment: .center) {
                    HStack (alignment: .center, spacing: 0) {
                        let tuple = contents()
                        Rectangle()
                            .fill(.blue)
                            .opacity(0.7)
                            .frame(width: 100, height: geo.size.height)
                            .overlay {
                                tuple.value.0
                            }
                        Rectangle()
                            .fill(.gray)
                            .opacity(0.7)
                            .frame(width: 100, height: geo.size.height)
                            .overlay {
                                tuple.value.1
                            }
                    }
                }
            }
        }
    }
    

    Also consider having separate initialiser parameters for the two views. I think it is clearer for the user of SeparateViewPanels to see that you expect exactly two views.

    let toolbar: () -> ToolBar
    let view2D: () -> View2D
    
    init(@ViewBuilder toolbar: @escaping () -> ToolBar, @ViewBuilder view2D: @escaping () -> View2D) {
        self.toolbar = toolbar
        self.view2D = view2D
    }
    
    var body: some View {
        GeometryReader { geo in
            VStack(alignment: .center) {
                HStack (alignment: .center, spacing: 0) {
                    Rectangle()
                        .fill(.blue)
                        .opacity(0.7)
                        .frame(width: 100, height: geo.size.height)
                        .overlay {
                            toolbar()
                        }
                    Rectangle()
                        .fill(.gray)
                        .opacity(0.7)
                        .frame(width: 100, height: geo.size.height)
                        .overlay {
                            view2D()
                        }
                }
            }
        }
    }
    

    The caller would do:

    SeparateViewPanels {
        View1()
    } view2D: {
        View2()
    }