iosswiftswiftuiswiftui-tabviewuiviewcontrollerrepresentable

Use VNDocumentCameraViewController as a UIViewControllerRepresentable in a TabView


I have VNDocumentCameraViewController as a UIViewControllerRepresentable in a view called ScanView embedded in a TabView as the 2nd screen. On dismissing the VNDocumentCameraViewController (either on cancelling or on saving of the scan), I want the tab view to get back to my first screen. That part works like a charm.

My issue is though that when coming back to my VNDocumentCameraViewController, I want to reinstantiate that controller and start over–which is what I cannot figure out on how to achieve that.

I am aware that my ContentView keeping a reference to the ScanView is why my UIViewControllerRepresentable is not reinstantiated–how can I do that manually?

Here's the code:

import SwiftUI

@main
struct so_VisionKitInTabsApp: App {
        var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {
    @State private var tabSelection = 1

    var body: some View {
        TabView(selection: $tabSelection) {
            Text("First View")
                .tabItem { Text("First View") }
                .tag(1)
            ScanView(tabSelection: $tabSelection)
                .tabItem { Text("Scan View") }
                .tag(2)
        }
    }
}

import VisionKit

struct DocumentScanningViewAdapter: UIViewControllerRepresentable {
    typealias UIViewControllerType = VNDocumentCameraViewController

    let onDismiss: () -> ()
        
    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }

    func makeUIViewController(context: Context) -> VNDocumentCameraViewController {
        let vc = VNDocumentCameraViewController()
        vc.delegate = context.coordinator
        return vc
    }
    
    func updateUIViewController(_ uiViewController: VNDocumentCameraViewController, context: Context) { }

    class Coordinator: NSObject, VNDocumentCameraViewControllerDelegate {
        var parent: DocumentScanningViewAdapter
        
        init(parent: DocumentScanningViewAdapter) {
            self.parent = parent
        }
        
        func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) {
            print("Finished successfully…")
            parent.onDismiss()
        }
        
        func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) {
            print("Cancelled…")
            resetCoordinator(for: controller)
            parent.onDismiss()
        }
        
        func resetCoordinator(for controller: VNDocumentCameraViewController) {
            controller.delegate = parent.makeCoordinator()
        }
    }
    
}

struct ScanView: View {
    @Binding var tabSelection: Int
    
    var body: some View {
        DocumentScanningViewAdapter(onDismiss: { tabSelection = 1 })
    }
}

Solution

  • This is how TabView works: it saves state of each tab

    You could've hack it, but please don't: when iOS user sees tab view he expects that if he switch from one tab to an other and back he won't loose any state

    Instead just make a button which present your document picker as a .sheet(...) or push in the navigation controller using NavigationLink, and your problem will be gone.

    If you use one of this approaches, you don't need to reset state of your controller because it's gonna be recreated each time you present a view

    In case you still wanna do that, you can wrap your controller with a UINavigationController and initialize your own controller it in updateUIViewController

    func makeUIViewController(context: Context) -> UINavigationController {
        let controller = UINavigationController()
        controller.isNavigationBarHidden = true
        return controller
    }
    
    func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
        let vc = VNDocumentCameraViewController()
        vc.delegate = context.coordinator
        uiViewController.viewControllers = [vc]
    }
    

    updateUIViewController gets called each time view re-render is needed. In case with TabView it's still not gonna work, because state is saved, to hack that you can add .id(tabSelection) to your ScanView