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