Search code examples
iosswiftconcurrency

How to convert `VNDocumentCameraViewControllerDelegate` to Swift 6 Concurrency


So I have this code:

fileprivate class DocumentScanDelegate: NSObject, VNDocumentCameraViewControllerDelegate {
  
  static let shared = DocumentScanDelegate()

  var compressionQuality: CGFloat = 1
  var onScanSuccess: (UIImage) -> Void = { _ in }
  
  func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) {
    
    controller.dismiss(animated: true)
    
    guard scan.pageCount >= 1 else { return }
    let lastPage = scan.imageOfPage(at: scan.pageCount - 1)
    let compressed = lastPage.compressed(quality: compressionQuality)
    onScanSuccess(compressed)
  }
  
  func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFailWithError error: Error) {
    controller.dismiss(animated: true)
  }
  
  func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) {
    controller.dismiss(animated: true)
  }
  

}

I got 2 errors:

  1. shared:

Static property 'shared' is not concurrency-safe because non-'Sendable' type 'DocumentScanDelegate' may have shared mutable state; this is an error in the Swift 6 language mode

  1. the 3 controller.dismiss calls:

Call to main actor-isolated instance method 'dismiss(animated:completion:)' in a synchronous nonisolated context; this is an error in the Swift 6 language mode

Both makes sense. So what I did is adding @MainActor to DocumentScanDelegate . Then first warning disappeared. But the second warning becomes:

Main actor-isolated instance method 'documentCameraViewController(_:didFinishWith:)' cannot be used to satisfy nonisolated protocol requirement; this is an error in the Swift 6 language mode

Then I use @preconcurrency to annotate VNDocumentCameraViewControllerDelegate conformance:

@MainActor
fileprivate class DocumentScanDelegate: NSObject, @preconcurrency VNDocumentCameraViewControllerDelegate {
  ...
}

This @preconcurrency trick is very similar to WWDC 2024's video (https://developer.apple.com/videos/play/wwdc2024/10169/?time=1520)

The error becomes:

Main actor-isolated instance method 'documentCameraViewController(_:didFinishWith:)' cannot be used to satisfy nonisolated protocol requirement; this is an error in the Swift 6 language mode

With the following sub-error:

Class 'VNDocumentCameraScan' does not conform to the 'Sendable' protocol

Then I use @preconcurrency import:

@preconcurrency import VisionKit

The same warning persists.

There's also a new warning (why?):

'@preconcurrency' attribute on module 'VisionKit' has no effect


Solution

  • Following up on our conversation in the comments here.

    Considering your last question, I'm not exactly sure why this works tbh, but I tried it with strict concurrency checking set to "complete" and it does not complain. My best guess is, that it's not an issue to capture controller in the Task closure, because it's guaranteed to be accessed only on the @MainActor there.

    import VisionKit
    
    private class DocumentScanDelegate: NSObject, VNDocumentCameraViewControllerDelegate {
        @MainActor
        static let shared = DocumentScanDelegate()
    
        var compressionQuality: CGFloat = 1
        var onScanSuccess: (UIImage) -> Void = { _ in }
    
        func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) {
            Task { @MainActor in 
                controller.dismiss(animated: true)
            }
            
            guard scan.pageCount >= 1 else { return }
            let lastPage = scan.imageOfPage(at: scan.pageCount - 1)
            let compressed = lastPage.compressed(quality: compressionQuality)
            onScanSuccess(compressed)
        }
    
        func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFailWithError error: Error) {
            Task { @MainActor in
                controller.dismiss(animated: true)
            }
        }
    
        func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) {
            Task { @MainActor in
                controller.dismiss(animated: true)
            }
        }
    }