swiftswiftuimetaclassmetatype

How can I use metatypes to show a View in SwiftUI?


I have a SwiftUI View that I would like to be able to show one of many different other views:

struct View1: View {
    var body: some View {
        Text("View1")
    }
}

struct View2: View {
    var body: some View {
        Text("View2")
    }
}

struct View3: View {
    var body: some View {
        Text("View3")
    }
}

struct View4: View {
    var body: some View {
        Text("View4")
    }
}
struct ContentView: View {
    @State var showView1 = false
    @State var showView2 = false
    @State var showView3 = false
    @State var showView4 = false

    @ViewBuilder var body: some View {
        if (showView1) {
            View1()
        } else if (showView2) {
            View2()
        } else if (showView3) {
            View3()
        } else if (showView4) {
            View4()
        } else {
            VStack {
                Button ("Show View1") {
                    showView1 = true
                }
                Button ("Show View2") {
                    showView2 = true
                }
                Button ("Show View3") {
                    showView3 = true
                }
                Button ("Show View4") {
                    showView4 = true
                }
            }
        }
    }
}

But there has got to be some way to use a metaclass to avoid the long if else chain.

So I tried something like:

struct ContentView: View {
    @State var showType: View.Type? = nil

    @ViewBuilder var body: some View {
        if let showType = showType {
            showType.init()
        } else {
            VStack {
                Button ("Show View1") {
                    showType = View1.type
                }
                Button ("Show View2") {
                    showType = View2.type
                }
                Button ("Show View3") {
                    showType = View3.type
                }
                Button ("Show View4") {
                    showType = View4.type
                }
            }
        }
    }
}

However this does not seem quite right as I get this error for the showType declaration:

Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements

I believe this is because View is a protocol and not a concrete type but I am not sure how to work around this. What would be the correct syntax to do this in Swift(UI)?


Solution

  • This is not an idiomatic thing to do in SwiftUI. If you just want to reduce clutter, I'd suggest using a switch and an enum value to represent each view.

    enum ShowableView {
        case one, two, three, four
    }
    
    @State private var shownView: ShowableView?
    
    var body: some View {
        switch shownView {
        case .one: View1()
        case .two: View2()
        case .three: View3()
        case .four: View4()
        case nil:
            VStack {
                Button("Show View1") {
                    shownView = .one
                }
                Button("Show View2") {
                    shownView = .two
                }
                Button("Show View3") {
                    shownView = .three
                }
                Button("Show View4") {
                    shownView = .four
                }
            }
        }
    }
    

    If you want to remove even the case labels, you can write a view that shows a particular view based on an Int index, but I would consider this a worse way of writing this than the switch with enums.

    struct ViewSelector<Content: View>: View {
        @Binding var selection: Int?
        let content: Content
        
        init(selection: Binding<Int?>, @ViewBuilder content: () -> Content) {
            self._selection = selection
            self.content = content()
        }
        
        var body: some View {
            Group(subviews: content) { subviews in
                if let selection {
                    subviews[selection]
                }
            }
        }
    }
    
    struct ContentView: View {
        @State private var shownView: Int?
        
        var body: some View {
            if shownView == nil {
                ViewSelector(selection: $shownView) {
                    View1()
                    View2()
                    View3()
                    View4()
                }
            } else {
                VStack {
                    Button("Show View1") {
                        shownView = 0
                    }
                    Button("Show View2") {
                        shownView = 1
                    }
                    Button("Show View3") {
                        shownView = 2
                    }
                    Button("Show View4") {
                        shownView = 3
                    }
                }
            }
        }
    }
    

    If you really want to store a meta type, you'd need to use AnyView, which can be problematic. This approach would only work with view types that have a parameterless initialiser, which greatly limits its usefulness compared to using a switch, or even the ViewSelector above.

    You'd first encode the parameterless initialiser requirement into a protocol,

    protocol DirectlyInitializableView: View {
        init()
    }
    
    extension DirectlyInitializableView {
        static func make() -> AnyView { AnyView(Self()) }
    }
    

    You can then store a (any DirectlyInitializableView.Type)?.

    @State private var shownView: (any DirectlyInitializableView.Type)?
    
    var body: some View {
        if let shownView {
            shownView.make()
        } else {
            VStack {
                // assuming these views all conform to DirectlyInitializableView
                Button ("Show View1") {
                    shownView = View1.self
                }
                Button ("Show View2") {
                    shownView = View2.self
                }
                Button ("Show View3") {
                    shownView = View3.self
                }
                Button ("Show View4") {
                    shownView = View4.self
                }
            }
        }
    }