Using switch/enums in SwiftUI Views - swiftui

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

Related

Menu active sheet SwiftUI

I'm trying to create a menu in SwiftUI with the possibility of showing sheets, but when I try to display the menu the sheets doesn't display.
Here's the code:
import SwiftUI
enum ActiveSheet: Identifiable {
case first, second
var id: Int {
hashValue
}
}
struct ContentView: View {
#State var activeSheet: ActiveSheet?
#State private var showingConfirmation = false
var body: some View {
Menu("Actions") {
VStack {
Button {
activeSheet = .first
} label: {
Text("Activate first sheet")
}
Button {
activeSheet = .second
} label: {
Text("Activate second sheet")
}
}
.sheet(item: $activeSheet) { item in
switch item {
case .first:
FirstView()
case .second:
SecondView()
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The actual result is this:
enter image description here
But without any possibility of access to the sheet pages I created.
Please let me know!
Move the .sheet to outside the Menu:
struct ContentView: View {
#State var activeSheet: ActiveSheet?
#State private var showingConfirmation = false
var body: some View {
Menu("Actions") {
VStack {
Button {
activeSheet = .first
} label: {
Text("Activate first sheet")
}
Button {
activeSheet = .second
} label: {
Text("Activate second sheet")
}
}
}
.sheet(item: $activeSheet) { item in
switch item {
case .first:
FirstView()
case .second:
SecondView()
}
}
}
}

NavigationLink match any enum associated value

How do I create a NavigationLink that matches any enum associated value?
I have this navigation enum for my app
enum NavRoute {
case loggedOut
case onboarding(OnboardingRoute)
enum OnboardingRoute {
case page1
case page2
}
}
#State var route = NavRoute.onboarding(.page1)
I want to write a NavigationLink like this
NavigationLink(
tag: NavRoute.onboarding(any), // What do I put here??
selection: $route
) {
OnboardingView()
} label: {
Text("Create account")
}
I don't think that is how you suppose to use this: NavigationLink(tag:selection:destination:label:) are suppose to work when a binding selection are the same of tag, so not when this case have some associated value. Like the example bellow: (It automatically navigate to two because of .onAppear modifier.
//
// ContentView.swift
// Navigation
//
// Created by Allan Garcia on 04/11/22.
//
import SwiftUI
enum NavigationRouteTag {
case one, two, three
}
struct ContentView: View {
#State var route: NavigationRouteTag?
var body: some View {
NavigationView {
List {
NavigationLink(
tag: .one,
selection: $route) {
Text("Hello World 1!")
} label: {
Text("Create Account 1!")
}
NavigationLink(
tag: .two,
selection: $route) {
Text("Hello World 2!")
} label: {
Text("Create Account 2!")
}
NavigationLink(
tag: .three,
selection: $route) {
Text("Hello World 3!")
} label: {
Text("Create Account 3!")
}
}
.onAppear {
route = .two
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
PS: This API is also deprecated in iOS16, maybe a different approach will be better for you like .navigationDestination(for:destination:) modifier.

SwiftUI 4: NavigationSplitView behaves strangely?

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

SwiftUI sheet does not dismiss

Using Swift5.2.3, iOS14.4.2, XCode12.4,
Working with the .sheet modifier in SwiftUI made me feel excited at first since it seemed like an easy and efficient way to display a modal sheet.
However, inside a real-world application it turns out that .sheet is all but ready for integration.
Here are two bugs found:
Bug 1: The sheet does not close sporadically
Bug 2: The Picker with DefaultPickerStyle does not work when inside a sheet's SegmentPicker (See this Stackoverlow-question that I created)
Let's focus now on Bug Nr1 : "sheet does not close":
The cmd presentationMode.wrappedValue.dismiss() is supposed to close a sheet. It works 90% of the cases. But every so often and without giving a hin on its reasons, the modal-sheet does not close.
Here is a code-excerpt:
import SwiftUI
import Firebase
struct MyView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Form {
Section(header: Text("Login")) {
Button(action: {
UserDefaults.standard.set(true, forKey: AppConstants.UserDefaultKeys.justLogoutLoginPressed)
try? Auth.auth().signOut()
// supposedly should work all the time - but it only works 90% of the time.....
presentationMode.wrappedValue.dismiss()
}) {
HStack {
Text((Auth.auth().currentUser?.isAnonymous ?? true) ? "Login" : "Logout")
Spacer()
}
}
}
}
.ignoresSafeArea()
Spacer()
}
}
}
I also tried to wrap the closing call inside the main-thread:
DispatchQueue.main.async {
presentationMode.wrappedValue.dismiss()
}
But it did not help.
Any idea why SwiftUI .sheets would not close using the presentationMode to dismiss it ??
Here I added the way the sheet is called in the first place. Since taken out of a bigger App, I obviously only show an example here on how the sheet is called:
import SwiftUI
#main
struct TestKOS005App: App {
#StateObject var appStateService = AppStateService(appState: .startup)
var body: some Scene {
WindowGroup {
MainView()
.environmentObject(appStateService)
}
}
}
class AppStateService: ObservableObject {
#Published var appState: THAppState
var cancellableSet = Set<AnyCancellable>()
init(appState: THAppState) {
self.appState = appState
}
// ...
}
enum THAppState: Equatable {
case startup
case downloading
case caching
case waiting
case content(tagID: String, name: String)
case cleanup
}
struct MainView: View {
#EnvironmentObject var appStateService: AppStateService
#State var sheetState: THSheetSelection?
init() {
UINavigationBar.appearance().tintColor = UIColor(named: "title")
}
var body: some View {
ZStack {
NavigationView {
ZStack {
switch appStateService.appState {
case .caching:
Text("caching")
case .waiting:
Text("waiting")
case .content(_, _):
VStack {
Text("content")
Button(action: {
sheetState = .sheetType3
}, label: {
Text("Button")
})
}
default:
Text("no screen")
}
}
.sheet(item: $sheetState) { state in
switch state {
case .sheetType1:
Text("sheetType1")
case .sheetType2:
Text("sheetType2")
case .sheetType3:
MyView()
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
enum THSheetSelection: Hashable, Identifiable {
case sheetType1
case sheetType2
case sheetType3
var id: THSheetSelection { self }
}
I think when signing out, you probably have an instance checking whether Firebase Auth has an active user session and changes the view to the login screen when you call try? Auth.auth().signOut() and it might prevent the presentationMode.wrappedValue.dismiss() is being called.
You might want to create a state property in MainView and a corresponding Binding property in MyView and manage the state of signing out with them like follows.
In the MyView; instead of calling signout() directly;
struct MyView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var logoutTapped: Bool
var body: some View {
VStack {
Form {
Section(header: Text("Login")) {
Button(action: {
UserDefaults.standard.set(true, forKey: AppConstants.UserDefaultKeys.justLogoutLoginPressed)
// try? Auth.auth().signOut() -> instead of this directly
logoutTapped = true // call this
// supposedly should work all the time - but it only works 90% of the time.....
presentationMode.wrappedValue.dismiss()
}) {
HStack {
Text((Auth.auth().currentUser?.isAnonymous ?? true) ? "Login" : "Logout")
Spacer()
}
}
}
}
.ignoresSafeArea()
Spacer()
}
}
}
and in the MainView, when creating sheet, in onDismissal block, set a condition on logoutTapped bool state, and logout there like below;
struct MainView: View {
#EnvironmentObject var appStateService: AppStateService
#State var sheetState: THSheetSelection?
#State var logoutTapped = false
init() {
UINavigationBar.appearance().tintColor = UIColor(named: "title")
}
var body: some View {
ZStack {
NavigationView {
ZStack {
switch appStateService.appState {
case .caching:
Text("caching")
case .waiting:
Text("waiting")
case .content(_, _):
VStack {
Text("content")
Button(action: {
sheetState = .sheetType3
}, label: {
Text("Button")
})
}
default:
Text("no screen")
}
}
.sheet(item: $sheetState) {
if logoutTapped { // if this is true call signout
Auth.auth().signout()
}
} content: { state in
switch state {
case .sheetType1:
Text("sheetType1")
case .sheetType2:
Text("sheetType2")
case .sheetType3:
MyView(logoutTapped: $logoutTapped) // send logoutTapped to MyView
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}

SwiftUI Switching between views

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