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 thepresentationMode.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;
and in the MainView, when creating sheet, in onDismissal block, set a condition on logoutTapped bool state, and logout there like below;