Search code examples
iosswiftswiftuiuikituihostingcontroller

SwiftUI view detect modal dismissal


I have a SwiftUI view wrapped in UIHostingController and presented modally as formSheet on iPad. I have a done button to dismiss and have a call back closure passed from parent view controller to perform actions when this done button is pressed. The problem happens on iPad where the user may tap outside of the view and the view gets dismissed thereby missing the callback.

One way to solve it would be to trigger the callback closure in onDismiss of the SwiftUI view. While it works, I am not sure if it is the right place and if it is reliable enough. The documentation only says that onDismiss is called when the "view disappears from the screen". However, what if we push a view on top of this view in a navigation hierarchy, will onDismiss be still called?


Solution

  • From your description, I assume you have a SwiftUI view like this:

    struct SomeView: View {
        let callback: () -> Void
        
        @Environment(\.dismiss) var dismiss
        
        var body: some View {
            NavigationStack {
                Text("Foo")
                    .toolbar {
                        ToolbarItem(placement: .topBarTrailing) {
                            Button("Done") {
                                callback()
                                dismiss()
                            }
                        }
                    }
            }
        }
    }
    

    When presenting the hosting controller, you can set its presentationController's delegate and implement the delegate method presentationControllerDidDismiss. For example:

    @objc func somethingIsClicked() {
        let host = UIHostingController(rootView: SomeView(callback: callback))
        host.modalPresentationStyle = .formSheet
        host.presentationController?.delegate = self
        present(host, animated: true)
    }
    
    func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
        callback()
    }
    
    func callback() {
        print("Dismissed!")
    }
    

    presentationControllerDidDismiss will be called when the sheet is completely dismissed, not when the user "starts" to dismiss it (which can be detected with presentationControllerWillDismiss), because at this point the user can change their mind and stop dismissing.

    As its documentation says, presentationControllerDidDismiss isn't called when the sheet is dismissed programmatically, so you still need to pass the callback to the SwiftUI view.


    Also note that you can set:

    host.isModalInPresentation = true
    

    to force the user to dismiss the sheet using your done button.