Reputation: 4848
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
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
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()
}
Upvotes: 2
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