Support swipe to dismiss for UIViewControllerRepresentable presented in sheet

Viewed 626

It seems if you use UIViewControllerRepresentable to implement a view controller in your SwiftUI app, when you present it via sheet you cannot swipe to dismiss it. Is there something you need to do to support swipe to dismiss?

struct ContentView: View {
    @State var showingPicker = false
    
    var body: some View {
        Text("Hello, world!")
            .onAppear {
                showingPicker = true
            }
            .sheet(isPresented: $showingPicker, content: {
                PHPicker() //cannot swipe to dismiss
                //Text("Test") //can swipe to dismiss
            })
    }
}

struct PHPicker: UIViewControllerRepresentable {
    func makeUIViewController(context: UIViewControllerRepresentableContext<PHPicker>) -> PHPickerViewController {
        let config = PHPickerConfiguration()
        return PHPickerViewController(configuration: config)
    }
    
    func updateUIViewController(_ uiViewController: PHPickerViewController, context: UIViewControllerRepresentableContext<PHPicker>) { }
}
1 Answers

Possible solution is to add something like handle to drag (no styling - simplified for demo),

demo

.sheet(isPresented: $showingPicker, content: {
        VStack {
            RoundedRectangle(cornerRadius: 8).fill(Color.gray)
                .frame(width: 60, height: 8)
                .padding(.top, 8)
            PHPicker()
        }
})

Alternate: the solution is to make presentation by UIKit completely and just pass activation binding inside representable.

Here is a demo of possible approach. Tested with Xcode 12.1 / iOS 14.1

demo2

struct PHPickerContentView: View {
    @State var showingPicker = false
    
    var body: some View {
        Button("Picker") {
           showingPicker = true
        }
        .background(PHPicker(isPresented: $showingPicker))    // << here !!
    }
}

struct PHPicker: UIViewControllerRepresentable {
    @Binding var isPresented: Bool

    func makeUIViewController(context: Context) -> UIViewController {
        UIViewController()   // << picker presenter
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        // react on binding & show if not shown
        if isPresented && uiViewController.presentedViewController == nil {
            let config = PHPickerConfiguration()
            let picker = PHPickerViewController(configuration: config)
            picker.delegate = context.coordinator

            uiViewController.present(picker, animated: true)
            picker.presentationController?.delegate = context.coordinator
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, PHPickerViewControllerDelegate, UIAdaptivePresentationControllerDelegate {
        let owner: PHPicker
        init(_ owner: PHPicker) {
            self.owner = owner
        }

        func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {

            // picked image handling code here
        
            picker.presentingViewController?.dismiss(animated: true)
            owner.isPresented = false    // << reset on action !!
        }
        
        func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
            owner.isPresented = false    // << reset on swipe !!
        }
    }
}
Related