Scenario:
- RootScreen presents DateScreen modally though
.sheet
- DateScreen has a DatePicker with CompactDatePickerStyle() and a button to dismiss the modal
- User opens the DatePicker
- User taps the DatePicker to bring up the NumPad for manual keyboard input
- User presses the button to dismiss the modal
SwiftUI will think the .sheet
got dismissed, but in reality, only the DatePicker’s modal got dismissed.
Minimum code example:
struct DateScreen: View {
@Binding var isPresented: Bool
@State var date: Date = Date()
var body: some View {
NavigationView {
VStack {
DatePicker("", selection: $date, displayedComponents: [.hourAndMinute])
.datePickerStyle(CompactDatePickerStyle())
}
.navigationBarItems(leading: Button("Dismiss") {
isPresented = false
})
}
}
}
@main
struct Main: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@State var isPresenting: Bool = false
var body: some Scene {
WindowGroup {
Button("Present modal", action: {
isPresenting = true
})
.sheet(isPresented: $isPresenting, content: {
DateScreen(isPresented: $isPresenting)
})
}
}
}
Gif showing the broken behavior:
Note, if the user doesn’t open the NumPad, it seems to work well.
3
Answers
The only workaround I found is to ignore SwiftUI and go back to UIKit to do the dismissal.
Instead of
isPresented = false
I have to doUIApplication.shared.windows.first?.rootViewController?.dismiss(animated: true)
.For iOS 15 this works to dismiss the sheet and doesn’t generate the warning:
'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a relevant window scene instead
code:
This is problem of provided code – the
State
is inScene
instead of view – state is not designed to update scene. The correct SwiftUI solution is to move everything from scene to a view and have only one root view there, ie.Tested with Xcode 13.4 / iOS 15.5