can I set a variable to @ViewBuilder?

1.6k Views Asked by At

Can onebody help me with the following please: I would like to be able to set a variable in a class conforming to "ObservableObject" to different Views in order to be able to pass that variable as "content" to custom view with @ViewBuilder

Here is an example:

class TopInfoController:ObservableObject {
    @Published var isDisplayed:Bool=false

    @Published var title=""
    
    //for calculated content it works - but this makes it not as flexible as I would like 
    //it to be
    @ViewBuilder var content:some View {
        VStack {
            Text("BLATESTBLA")
        }
    }

    //I would like to be able to set this from outside, not to be just calculated
    //something like this: 
    //@ViewBuilder var content:(some View)?
    //i can do var test:String?, why can't I seem to be able (some View)?
    //and set it later on when needed

    
}

struct ContentView: View {
    
    @StateObject var cntrl=TopInfoController()

    var body: some View {
        ZStack {
            MainDisplay()
                .environmentObject(cntrl)
            TopInfo()
                .environmentObject(cntrl)
                .opacity(cntrl.isDisplayed ? 1:0)
        }
    }
}

struct TopInfo:View {
    @EnvironmentObject var cntrl:TopInfoController
    
    var body: some View {
        VStack {
            MyNotification(title: cntrl.title) {
                //this is where I need to be able to display any content (View)
                //that I would like to be able to set from whereever in the app that
                //would have access to
                //"TopInfoController" observableobject
                cntrl.content
            }
        }
    }
}

struct MyNotification<Content:View>:View {
    
    let title:String
    let content:Content
    
    init(title: String, @ViewBuilder content:()->Content) {
        self.title=title
        self.content=content()
    }
    
    var body: some View {
        VStack {
            Text(title)
            content
        }
    }
}

struct MainDisplay:View {
    
    @EnvironmentObject var cntrl:TopInfoController
    var body: some View {
        VStack {
            Text("TITLE")
            Spacer()
            Button {
                cntrl.title="TEST"

                //!!!!!
                //here, I would like to be able to do something like
                //cntrl.content=self.notificationContent
                cntrl.isDisplayed.toggle()
            } label: {
                Text("Display")
            }
        }
    }
    
    @ViewBuilder var notificationContent:some View {
        VStack {
            Text("this is my notification")
            Image(systemName: "heart.fill")
        }
    }
}

Is there a way to create a variable that would hold optional type (some View)? Based on what I need to display, I would set this variable.

Thanks a lot for any explanation that would help me understand how to do it or why it is not possible.

Libor

Working example using AnyView

class TopInfoController:ObservableObject {
    @Published var isDisplayed:Bool=false
    
    @Published var title:String=""
    @Published var generalAlertView:AnyView?
    
    
}
struct TopInfo:View {
    @EnvironmentObject var cntrl:TopInfoController
    
    var body: some View {
        VStack {
            MyNotification(title: cntrl.title) {
                cntrl.generalAlertView
            }
        }
    }
}

struct MyNotification<Content:View>:View {
    
    let title:String
    let content:Content
    
    init(title: String, @ViewBuilder content:()->Content) {
        self.title=title
        self.content=content()
    }
    
    var body: some View {
        VStack {
            Text(title)
            content
        }
    }
}

struct ContentView: View {

    @StateObject var cntrl=TopInfoController()

    var body: some View {
        ZStack {
//            Example()
            MainDisplay()
                .environmentObject(cntrl)
            TopInfo()
                .environmentObject(cntrl)
                .opacity(cntrl.isDisplayed ? 1:0)
        }
    }
}

struct MainDisplay:View {
    
    @EnvironmentObject var cntrl:TopInfoController
    
    var body: some View {
        VStack {
            Text("TITLE")
            Spacer()
            HStack {
                Button {
                    cntrl.title="Test 1"
                    cntrl.generalAlertView=AnyView(notification1)
                    cntrl.isDisplayed=true
                } label: {
                    Text("Display 1")
                }
                .tint(.green)
                .buttonStyle(.borderedProminent)
                .buttonBorderShape(.capsule)
                
                Button {
                    cntrl.title="Test 2"
                    cntrl.generalAlertView=AnyView(notification2)
                    cntrl.isDisplayed=true
                } label: {
                    Text("Display 2")
                }
                .tint(.red)
                .buttonStyle(.borderedProminent)
                .buttonBorderShape(.capsule)
            }
        }
    }
    
    @ViewBuilder private var notification1:some View {
        VStack {
            Text("this is my notification")
            Image(systemName: "heart.fill")
        }
    }
    
    @ViewBuilder private var notification2:some View {
        HStack {
            Text("Something else to display")
            Image(systemName: "circle.fill")
        }
    }
}

'''

1

There are 1 best solutions below

4
On

View is a specific protocol, not a type; not all instances are the same: a VStack is different than a Text. SwiftUI doesn't know how to treat the variable until you use it. This means that you can't have a variable that contains a "modifiable" view.

You can instead define a view that has a different content for each instance. The variable inside that view will not be of type View, but will be a function that returns a type of some View.

The code below is an example:

// A view that can receive another view as a parameter.
// The parameter is of a type that conforms to View.
struct ContentContainer<T: View>: View {
    
    let text: String
    
    // The custom view passed is not of type View, nor T:
    // it is a function that returns a type that conforms to View
    let content: ()->T
    
    var body: some View {
        VStack {
            Text(text)
            content()
                .foregroundColor(.red)
        }
        .padding()
    }
}

Usage:

struct Example: View {
    
    @State private var custom = ContentContainer(text: "Custom") { Text("View") }
    
    var body: some View {
        VStack {
            
            // Use the custom view
            ContentContainer(text: "Will make this red:") {
                Text("Text turned to red")
            }

            // Use the custom view with another instance
            ContentContainer(text: "Drawing a circle") {
                // A red circle
                Circle()
                    .frame(width: 200, height: 200)
            }
            
            custom
            
            // This will NOT work:
            // The type was already defined as "Text" when you created the instance, even if you
            // set "content" as a var instead of a let inside ContentContainer
            // Button {
            //     custom.content = { Circle().frame(width: 100, height: 100)}
            // } label: {
            //     Text("Change")
            // }
        }
    }
}