CPlus
CPlus

Reputation: 4848

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

Upvotes: 0

Views: 59

Answers (3)

2pp
2pp

Reputation: 41

Other than enum from the accepted answer, you can use something like this:

struct ContentView: View {
    @State private var shownView: (any View)?

    var body: some View {
        if let shownView {
            AnyView(shownView)
        } else {
            VStack {
                Button("Show View1") {
                    shownView = View1()
                }
                Button("Show View2") {
                    shownView = View2()
                }
                Button("Show View3") {
                    shownView = View3()
                }
                Button("Show View4") {
                    shownView = View4()
                }
            }
        }
    }
}

But still, enum is preferred.

Upvotes: 0

Andrei G.
Andrei G.

Reputation: 1557

To show views in place, you can simply use an enum that conforms to :View.

In your main view, you create an instance of that enum and set its value using the buttons to any of the cases available in the enum.

Here's a working example:

import SwiftUI

enum ViewPresenter: View {
    case view1, view2, view3, view4
    
    var body: some View {
        switch self {
            case .view1:
                View1()
            case .view2:
                View2()
            case .view3:
                View3()
            case .view4:
                View4()
        }
    }
}

struct ShowViewsDemo: View {
    @State var presentView: ViewPresenter?

    var body: some View {
        VStack {
            if presentView != nil {
                //Show selected view
                presentView
                    .foregroundStyle(.white)
                
                    //Close button as overlay
                    .overlay(alignment: .topTrailing) {
                        Button {
                            presentView = nil
                        } label: {
                            Image(systemName: "xmark.circle.fill")
                                .font(.title2)
                        }
                        .padding(.horizontal)
                        .tint(.white)
                    }
            }
            else {
                //Show placeholder
                ContentUnavailableView {
                    Label("No view selected", systemImage: "eye")
                } description: {
                    Text("Tap any of the buttons to select and show a view.")
                }
            }
            
            Spacer() // push buttons to the bottom
            
            //Buttons
            HStack {
                Button ("View1") {
                    presentView = .view1
                }
                Button ("View2") {
                    presentView = .view2
                }
                Button ("View3") {
                    presentView = .view3
                }
                Button ("View4") {
                    presentView = .view4
                }
            }
            .padding()
            .buttonStyle(.bordered)
        }
        .animation(.smooth, value: presentView)
    }
}

struct View1: View {
    
    var body: some View {
        
        VStack {
            Text("View 1")
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(.green)
    }
}

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

        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(.orange)
    }
}

struct View3: View {
    
    var body: some View {
        
        VStack {
            Text("View 3")
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(.blue)
    }
}

struct View4: View {
    
    var body: some View {
        
        VStack {
            Text("View 4")
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(.pink)
    }
}

#Preview {
    ShowViewsDemo()
}

enter image description here

Upvotes: 2

Sweeper
Sweeper

Reputation: 273510

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

Upvotes: 1

Related Questions