0

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

2 Answers 2

0

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")
            // }
        }
    }
}
0

The some View is not a type but indicator for compiler that it should detect type from output by itself.

Instead a possible approach for such kind of tasks is to use generics, like

class TopInfoController<Content:View> : ObservableObject {
    @Published var isDisplayed:Bool=false

    @Published var title=""

    var content: (() -> Content)?

    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content
    }

    init(content: (() -> Content)?) {
        self.content = content
    }
}

Tested with Xcode 13.2

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.