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.
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.