I learned that I shouldn't be using AnyView() in SwiftUI. I have written many functions with switch statements to return corresponding Views like the below code. I'm trying to learn the best way to migrate the switch statements away from AnyView()
struct T01: View{
var body: some View{
VStack{
showView(i: 1)
}
}
func showView(i: Int) -> some View{
switch(i){
case 0: return AnyView(viewOne)
case 1: return AnyView(viewTwo)
default: return AnyView(EmptyView())
}
}
var viewOne: some View {
Text("View One")
}
var viewTwo: some View {
Text("View Two")
}
}
Thanks for any help!!!
Your showView function has to return the same type of view each time, which is why AnyView works.
You have a couple of options to fix this:
Wrap everything in another View, e.g. a Group
func showView(i: Int) -> some View {
Group {
switch i {
case 0:
viewOne
case 1:
viewTwo
default:
EmptyView()
}
}
}
Mark the function with the #ViewBuilder attribute
#ViewBuilder
func showView(i: Int) -> some View {
switch i {
case 0:
viewOne
case 1:
viewTwo
default:
EmptyView()
}
}
Related
In SwiftUI 4, there is now a NavigationSplitView. I played around with it and detected some strange behaviour.
Consider the following code: When the content function returns the plain Text, then there is the expected behaviour - tapping a menu item changes the detail view to the related text.
However, when commenting out the first four cases, and commenting in the next four, then a tap on "Edit Profile" does not change the detail view display. (Using #ViewBuilder does not change this behaviour.)
Any ideas out there about the reasons for that? From my point of view, this may just be a simple bug, but perhaps there are things to be considered that are not documented yet?!
struct MainScreen: View {
#State private var menuItems = MenuItem.menuItems
#State private var menuItemSelection: MenuItem?
var body: some View {
NavigationSplitView {
List(menuItems, selection: $menuItemSelection) { course in
Text(course.name).tag(course)
}
.navigationTitle("HappyFreelancer")
} detail: {
content(menuItemSelection)
}
.navigationSplitViewStyle(.balanced)
}
func content(_ selection: MenuItem?) -> some View {
switch selection {
case .editProfile:
return Text("Edit Profile")
case .evaluateProfile:
return Text("Evaluate Profile")
case .setupApplication:
return Text("Setup Application")
case .none:
return Text("none")
// case .editProfile:
// return AnyView(EditProfileScreen())
//
// case .evaluateProfile:
// return AnyView(Text("Evaluate Profile"))
//
// case .setupApplication:
// return AnyView(Text("Setup Application"))
//
// case .none:
// return AnyView(Text("none"))
}
}
}
struct MainScreen_Previews: PreviewProvider {
static var previews: some View {
MainScreen()
}
}
enum MenuItem: Int, Identifiable, Hashable, CaseIterable {
var id: Int { rawValue }
case editProfile
case evaluateProfile
case setupApplication
var name: String {
switch self {
case .editProfile: return "Edit Profile"
case .evaluateProfile: return "Evaluate Profile"
case .setupApplication: return "Setup Application"
}
}
}
extension MenuItem {
static var menuItems: [MenuItem] {
MenuItem.allCases
}
}
struct EditProfileScreen: View {
var body: some View {
Text("Edit Profile")
}
}
After playing around a bit in order to force SwiftUI to redraw the details view, I succeeded in this workaround:
Wrap the NavigationSplitView into a GeometryReader.
Apply an .id(id) modifier to the GeometryReader (e.g., as #State private var id: Int = 0)
In this case, any menu item selection leads to a redraw as expected.
However, Apple should fix the bug, which it is obviously.
I've found that wrapping the Sidebar list within its own view will fix this issue:
struct MainView: View {
#State var selection: SidebarItem? = .none
var body: some View {
NavigationSplitView {
Sidebar(selection: $selection)
} content: {
content(for: selection)
} detail: {
Text("Detail")
}
}
#ViewBuilder
func content(for item: SidebarItem?) -> some View {
switch item {
case .none:
Text("Select an Item in the Sidebar")
case .a:
Text("A")
case .b:
Text("B")
}
}
}
So I want to have a view which presents conditionally, depending on the state of my model. I understand how to do this if each case has a view to present, but how do I handle the case where I want to show no view in some cases?
For instance:
struct MyView: View {
enum ViewState {
case A, B, C
}
let state: ViewState
var view: some View {
switch state {
case .A:
Text("A State")
case .B:
Text("B State")
case .C:
// empty
}
}
}
You can to use EmptyView and (!) mark view property as a ViewBuilder, like in below finalised example:
struct MyView: View {
enum ViewState {
case A, B, C
}
let state: ViewState
#ViewBuilder var view: some View { // << here !!
switch state {
case .A:
Text("A State")
case .B:
Text("B State")
case .C:
EmptyView() // << here !!
}
}
var body: some View { // replicated for demo
self.view
}
}
Tested with Xcode 13.2
You can use EmptyView() and mark you view with #ViewBuilder var view: some View
struct MyView: View {
enum ViewState {
case A, B, C
}
let state: ViewState
var text: String {
switch state {
case .A:
"A State"
case .B:
"B State"
case .C:
""
}
}
var view: some View {
Text(text)
}
}
As the time flies, by App get more and more complicated shape.
In some cases the App flow might be:
View A -> View B -> C -> D
and then back
D -> C -> B -> A..
but sometimes i need to skip the view and go
D -> B -> A..
In some cases its A -> C -> D and then D -> A
I started to use NavigationView/NavigationLink and in some cases i use the following approach:
let weekView = WeekView(journey: journey, isRoot: isRoot).environmentObject(self.thisSession)
window?.rootViewController = UIHostingController(rootView: weekView)
No i realize that it has become a complete mess.. It's time for me to rethink this..
How do you handle navigation in apps where it can't be always done by pushing/popping the views from Navigation stack?
Using ViewBuilders is a good option here.
#ViewBuilder func myViewRouter(selection: Selection) -> some View {
switch selection {
case selection1:
View1()
case selection2:
View2()
case selection3:
View3()
}
}
enum Selection { ... }
ViewBuilders are powerful, its pretty much a function that can return opaque types but notice the lack of the return keyword. This seems like a perfect use case for it. In the example I used a enum but its also common to see this used with a var selection = 0 on the parent view and have the ViewBuilder as the child. Either way, same functionality.
Below is is a good url to understand ViewBuilders.
https://swiftwithmajid.com/2019/12/18/the-power-of-viewbuilder-in-swiftui/
Last edit: Here is an example use case:
import SwiftUI
struct ContentView: View {
#State private var selection: SelectionEnum = .zero
var body: some View {
VStack {
showMyViews(selection: selection)
HStack {
ForEach(SelectionEnum.allCases, id: \.self) { selection in
Button(action: {self.selection = selection}){
Text(selection.rawValue)
.fontWeight(.bold)
.frame(width: 60, height: 60)
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(10)
}
}
}
}
}
#ViewBuilder func showMyViews(selection: SelectionEnum) -> some View {
switch selection {
case .zero:
ViewA()
case .one:
ViewB()
case .two:
ViewC()
case .three:
ViewD()
}
}
enum SelectionEnum: String, CaseIterable {
case zero
case one
case two
case three
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ViewA: View {
var body: some View {
Text("View A")
}
}
struct ViewB: View {
var body: some View {
Text("View B")
}
}
struct ViewC: View {
var body: some View {
Text("View C")
}
}
struct ViewD: View {
var body: some View {
Text("View D")
}
}
I have a parent view (Content View) containing an array of integers,
The view switcher function inside the parent switches the child view depending on the value of the integer in the array.
The issue I have is is that 'view1' is re-presented (after being presented before) the view is not redrawn and the text in the textfield remains populated.
How can I redraw the child view each time the switch function is called?
thanks
struct ContentView: View {
var views = [2,1,1]
#State var currentView = 0
var body: some View {
VStack{
viewSwitcher()
}
}
func viewSwitcher() -> AnyView {
switch views[currentView] {
case 1:
return AnyView(view1(currentView: self.$currentView))
case 2:
return AnyView(view2(currentView: self.$currentView))
default:
return AnyView(EmptyView())
}
}
}
struct view1:View {
#State var textInput: String = ""
#Binding var currentView: Int
var body: some View {
VStack{
Text("View 1")
TextField("Enter Text", text: self.$textInput)
Button(action: {
self.currentView += 1
}){
Text("Submit")
}
}
}
}
struct view2:View {
#Binding var currentView: Int
var body: some View {
VStack{
Text("View 2")
Button(action: {
self.currentView += 1
}){
Text("Submit")
}
}
}
}
Here is possible solution. Using .id modifier makes view unique per-update, so rebuilds.
Tested with Xcode 11.4 / iOS 13.4
func viewSwitcher() -> AnyView {
switch views[currentView] {
case 1:
return AnyView(view1(currentView: self.$currentView).id(UUID()))
case 2:
return AnyView(view2(currentView: self.$currentView).id(UUID()))
default:
return AnyView(EmptyView())
}
}
As of Xcode 11.4, SwiftUI doesn't allow switch statements in Function builder blocks like VStack {}, failing with a generic error like Generic parameter 'Content' could not be inferred. How can the switch statement be used in SwiftUI to create different Views depending on an enum value?
switch in SwiftUI view builders is supported since Xcode 12:
enum Status {
case loggedIn, loggedOut, expired
}
struct SwiftUISwitchView: View {
#State var userStatus: Status = .loggedIn
var body: some View {
VStack {
switch self.userStatus {
case .loggedIn:
Text("Welcome!")
case .loggedOut:
Image(systemName: "person.fill")
case .expired:
Text("Session expired")
}
}
}
}
You can use enum with #ViewBuilder as follow ...
Declear enum
enum Destination: CaseIterable, Identifiable {
case restaurants
case profile
var id: String { return title }
var title: String {
switch self {
case .restaurants: return "Restaurants"
case .profile: return "Profile"
}
}
}
Now in the View file
struct ContentView: View {
#State private var selectedDestination: Destination? = .restaurants
var body: some View {
NavigationView {
view(for: selectedDestination)
}
}
#ViewBuilder
func view(for destination: Destination?) -> some View {
switch destination {
case .some(.restaurants):
CategoriesView()
case .some(.profile):
ProfileView()
default:
EmptyView()
}
}
}
If you want to use the same case with the NavigationLink ... You can use it as follow
struct ContentView: View {
#State private var selectedDestination: Destination? = .restaurants
var body: some View {
NavigationView {
List(Destination.allCases,
selection: $selectedDestination) { item in
NavigationLink(destination: view(for: selectedDestination),
tag: item,
selection: $selectedDestination) {
Text(item.title).tag(item)
}
}
}
}
#ViewBuilder
func view(for destination: Destination?) -> some View {
switch destination {
case .some(.restaurants):
CategoriesView()
case .some(.profile):
ProfileView()
default:
EmptyView()
}
}
}