Search code examples
swiftswiftuiconcurrencyswift6

Swift 6: Error: Task or actor isolated value cannot be sent


I'm trying to resolve a compilation error: Task or actor isolated value cannot be sent when using Xcode 16.0 beta 3 with Swift 6 and Complete Concurrency enabled. My code:

import SwiftUI
@preconcurrency import Contacts
import ContactsUI

struct SimpleContactPicker: View {
    
    let allowedContactType: CNContactType? = CNContactType.person
    
    @Binding var selectedContacts: [CNContact]?

    var body: some View {
        SimpleContactPickerProxy(allowedContactType: self.allowedContactType, selectedContacts: $selectedContacts)
            .ignoresSafeArea()
    }
}

extension CNContactType {
    
    var predicateForEnablingContact: NSPredicate {
        
        NSPredicate(format: "contactType=\(self.rawValue)")
    }
}

struct SimpleContactPickerProxy: UIViewControllerRepresentable, @unchecked Sendable {
    
    typealias UIViewControllerType = CNContactPickerViewController
    
    let allowedContactType: CNContactType?
    
    @Binding var selectedContacts: [CNContact]?

    final class Coordinator: NSObject, CNContactPickerDelegate, @unchecked Sendable {
        var parent: SimpleContactPickerProxy

        init(_ parent: SimpleContactPickerProxy) {
            self.parent = parent
        }
        
        func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
        }

        func contactPicker(_ picker: CNContactPickerViewController, didSelect contacts: [CNContact]) {
           
            Task { @MainActor in
                self.parent.selectedContacts = contacts // <<-- ERROR: Task or actor isolated value cannot be sent
            }
        }
    }
    
    func makeUIViewController(context: Context) -> CNContactPickerViewController {
        
        let contactPickerViewController = CNContactPickerViewController()
        
        contactPickerViewController.delegate = context.coordinator
        
        if let allowedContactType {
            contactPickerViewController.predicateForEnablingContact = allowedContactType.predicateForEnablingContact
        }
        
        return contactPickerViewController
    }
    
    func updateUIViewController(_ uiViewController: CNContactPickerViewController, context: Context) {
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
}

While I generally understand that the [CNContact] array cannot be sent from the non-isolated context to the MainActor context because CNContact is non-Sendable, it's unclear how to work around this problem. (This issue did not appear in earlier Xcode 16 betas).

I guess it's possible to create my own CNContact facsimile struct with copied data, but this seems like an annoying amount of overhead. I also tried MainActor.assumeIsolated and other strategies discussed in this WWDC 2024 Swift Concurrency video https://developer.apple.com/wwdc24/10169, but none of these approaches appeared to work.

Any help is appreciated.


Solution

  • The first thing you should know what what types can be declared as Sendable safely.

    Value types

    Reference types with no mutable storage

    Reference types that internally manage access to their state

    Functions and closures (by marking them with @Sendable)

    marking CNContact as Sendable is ok

    A CNContact object stores an immutable copy of a contact’s information, so you cannot change the information in this object directly. Contact objects are thread-safe, so you may access them from any thread of your app.

    Note that View and Coordinator can be made safe with @MainActor they don't require Sendable.

    import SwiftUI
    import Contacts
    import ContactsUI
    
    struct ContactsView: View {
        @State private var selected: [CNContact] = []
        var body: some View {
            SimpleContactPicker(selectedContacts: $selected)
        }
    }
    
    #Preview {
        ContactsView()
    }
    
    
    
    struct SimpleContactPicker: View {
        
        let allowedContactType: CNContactType? = CNContactType.person
        
        @Binding var selectedContacts: [CNContact]
    
        var body: some View {
            SimpleContactPickerProxy(allowedContactType: self.allowedContactType, selectedContacts: $selectedContacts)
                .ignoresSafeArea()
        }
    }
    
    struct SimpleContactPickerProxy: UIViewControllerRepresentable {
        
        typealias UIViewControllerType = CNContactPickerViewController
        
        let allowedContactType: CNContactType?
        
        @Binding var selectedContacts: [CNContact]
        
        @MainActor
        final class Coordinator: NSObject, CNContactPickerDelegate {
            var parent: SimpleContactPickerProxy
    
            init(_ parent: SimpleContactPickerProxy) {
                self.parent = parent
            }
            
            nonisolated func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
            }
    
            nonisolated func contactPicker(_ picker: CNContactPickerViewController, didSelect contacts: [CNContact]) {
               
                Task { @MainActor in
                    parent.selectedContacts = contacts
                }
            }
        }
        
        func makeUIViewController(context: Context) -> CNContactPickerViewController {
            
            let contactPickerViewController = CNContactPickerViewController()
            
            contactPickerViewController.delegate = context.coordinator
            
            if let allowedContactType {
                contactPickerViewController.predicateForEnablingContact = allowedContactType.predicateForEnablingContact
            }
            
            return contactPickerViewController
        }
        
        func updateUIViewController(_ uiViewController: CNContactPickerViewController, context: Context) {
        }
        
        func makeCoordinator() -> Coordinator {
            Coordinator(self)
        }
    }
    
    
    extension CNContactType {
        
        var predicateForEnablingContact: NSPredicate {
            
            NSPredicate(format: "contactType=\(self.rawValue)")
        }
    }
    ///Immutable & thread-safe
    extension CNContact: @unchecked @retroactive Sendable {}
    

    Also note that

    final class Coordinator: NSObject, CNContactPickerDelegate, @unchecked Sendable
    

    is incorrect because it does not comply with any of the accepted forms of Sendable it is a reference type with mutable storage.