SwiftUI sheet does not dismiss

756 views Asked by At

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

There are 1 answers

0
Ismail Gok On

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