7

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.

Gif showing broken behavior

Lord Zsolt
  • 6,492
  • 9
  • 46
  • 76
  • Likely has to do with the whole root view controller thing (everything presents there). If you use `.adaptiveSheet` from [here](https://stackoverflow.com/questions/56700752/swiftui-half-modal/67994666#67994666) instead it works fine. Likely not a solution. – lorem ipsum Dec 19 '21 at 20:01
  • 1
    This also breaks the latest (iOS15) SwiftUI `@Environment(\.dismiss)` dismiss action. – Andrew Tetlaw Jan 02 '22 at 01:06
  • 1
    I should also add, that it works fine in the iOS simulator. Seems to only be a problem on a device. – Andrew Tetlaw Jan 02 '22 at 01:13

3 Answers3

1

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 do UIApplication.shared.windows.first?.rootViewController?.dismiss(animated: true).

Lord Zsolt
  • 6,492
  • 9
  • 46
  • 76
  • This isn't really a great solution because if you have multiple layers of sheets it will dismiss all of them, not only the topmost one where the date picker is. – axlrtr Mar 27 '23 at 01:08
1

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:

UIApplication.shared.connectedScenes
    .filter({$0.activationState == .foregroundActive})
    .compactMap({$0 as? UIWindowScene})
    .first?
    .windows
    .first { $0.isKeyWindow }?
    .rootViewController?
    .dismiss(animated: true)
Benson Wong
  • 631
  • 6
  • 5
  • This isn't really a great solution because if you have multiple layers of sheets it will dismiss all of them, not only the topmost one where the date picker is. – axlrtr Mar 27 '23 at 01:07
  • Add .presentedViewController? before dismiss and it will only dismiss the topmost sheet. – axlrtr Apr 17 '23 at 00:18
1

This is problem of provided code - the State is in Scene 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

demo

@main
struct Main: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
           ContentView()    // << window root view, the one !!
        }
    }
}


struct ContentView: View {
    @State var isPresenting: Bool = false

    var body: some View {
        Button("Present modal", action: {
            isPresenting = true
        })
        .sheet(isPresented: $isPresenting, content: {
            DateScreen(isPresented: $isPresenting)
        })

    }
}

// no more changes needed
Asperi
  • 228,894
  • 20
  • 464
  • 690