Search code examples
swiftxcodeswiftuiswift6

Swift 6, 'image' causes data races?


I have an image picker struct that I use to take and store photos as a list however with Swift 6 strict concurrancy I can't for the life of me get it to work.

This is the original struct

struct ImagePickerView: UIViewControllerRepresentable {
    class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate, PHPickerViewControllerDelegate {
        var parent: ImagePickerView

        init(_ parent: ImagePickerView) {
            self.parent = parent
        }

        // Handle image picker for camera capture
        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
            if let image = info[.originalImage] as? UIImage {
                if let imageData = image.jpegData(compressionQuality: 0.6) {
                    parent.imageList.append(ImageData(
                        _id: "\(UUID())",
                        imageData: imageData
                    ))
                }
            }
            picker.dismiss(animated: true)
        }

        // Handle picker for selecting from library
        func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
            picker.dismiss(animated: true)

            guard let provider = results.first?.itemProvider else { return }
            if provider.canLoadObject(ofClass: UIImage.self) {
                provider.loadObject(ofClass: UIImage.self) { image, _ in
                    DispatchQueue.main.async {
                        if let imageData = (image as? UIImage)?.jpegData(compressionQuality: 0.6) {
                            self.parent.imageList.append(ImageData(
                                _id: "\(UUID())",
                                imageData: imageData
                            ))
                        }
                    }
                }
            }
        }
    }

    @Binding var imageList: [ImageData]
    @Binding var isCamera: Bool

    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }

    func makeUIViewController(context: Context) -> UIViewController {
        if isCamera {
            // Camera access using UIImagePickerController
            let imagePicker = UIImagePickerController()
            imagePicker.sourceType = .camera
            imagePicker.delegate = context.coordinator
            return imagePicker
        } else {
            // Photo Library using PHPickerViewController
            var config = PHPickerConfiguration()
            config.selectionLimit = 1
            config.filter = .images

            let picker = PHPickerViewController(configuration: config)
            picker.delegate = context.coordinator
            return picker
        }
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

Within the picker function, the if let imageData = (image as? UIImage)?, the image is flagged with,

Sending 'image' risks causing data races.

Task-isolated 'image' is captured by a main actor-isolated closure. main actor-isolated uses in closure may race against later nonisolated uses.

I'm struggling to understand how to fix this error, I've attempted to run the dispatchQueue as a Task { @MainActor in } but that leads to the self.parent saying that sending self causes data races.


Solution

  • You are trying to solve Swift concurrency isolation problems with GCD, which is generally not a good idea.

    The loadObject completion handler requires Sendable conformance since it can be passed across concurrency domains. This also means that all captured values must conform to Sendable.

    See SE-0302 for more details.

    Your Coordinator is already bound to the MainActor by the way it is declared and through the protocol conformances. This means that Coordinator already conforms to Sendable.

    So you don't have to do anything in this regard. All you have to do is adjust the call in the completion handler:

    // Handle picker for selecting from library
    func picker(
        _ picker: PHPickerViewController,
        didFinishPicking results: [PHPickerResult]
    ) {
        picker.dismiss(animated: true)
    
        guard let provider = results.first?.itemProvider, provider.canLoadObject(ofClass: UIImage.self) else {
            return
        }
    
        provider.loadObject(ofClass: UIImage.self) { image, _ in
            guard let imageData = (image as? UIImage)?.jpegData(compressionQuality: 0.6) else {
                return
            }
    
            Task { @MainActor in
                self.parent.imageList.append(ImageData(
                    _id: UUID().uuidString,
                    imageData: imageData
                ))
            }
        }
    }
    

    Note that this captures self in the completion handler and the task in it. If you want to prevent this or have a cancel mechanism, you must add this accordingly. However, this was not part of your actual question, so I won't go into it in detail.