skip to Main Content

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

3

Answers


  1. Chosen as BEST ANSWER

    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).


  2. 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)
    
    Login or Signup to reply.
  3. 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
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search