Search code examples
swiftconcurrency

Concurrency, Reference to captured var in concurrently-executing code


I want to fetch user's contacts using enumerateContacts(with:usingBlock:) and async/await method. Here is my function:

func fetchContacts() async throws -> [Contact] {
        let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey]
        let request = CNContactFetchRequest(keysToFetch: keys as [CNKeyDescriptor])
        let store = CNContactStore()
        let contactsActor = ContactsActor()
        var contactsArray: [Contact] = []

        return try await withCheckedThrowingContinuation { continuation in
           DispatchQueue.global(qos: .background).async {
                do{
                    try  store.enumerateContacts(with: request) { contact, stop in
                        let contact = Contact(
                            givenName: contact.givenName,
                            familyName: contact.familyName,
                            emails: contact.emailAddresses.map { $0.value as String }
                        )
                        Task {
                             await contactsActor.appendToContacts(contact: contact)
                        }
                    }
                    continuation.resume(returning: contactsArray )
                } catch {
                    continuation.resume(throwing: error)
                }
            }
        }
    }

Also I am using this actor:

actor ContactsActor {
    var contacts:[Contact] = []

    func appendToContacts(contact: Contact) {
        contacts.append(contact)
    }
    
    func getContact()-> [Contact]{
       return contacts
    }
}

In the viewDidLoad method I call the fetchContacts function inside a Task:

Task {
    let contacts = try await fetchContacts()

    await MainActor.run(body: {
        self.contactsTable.reloadData()
    })
}

In front of continuation.resume(returning: contactsArray ) I am getting this error:

Reference to captured var 'contactsArray' in concurrently-executing code.

I am learning Swift concurrency and I do not know exactly how to solve this error.

I was expecting to get the user's contacts in an array of custom struct named Contact.


Solution

  • I would avoid the GCD API, and instead follow Swift concurrency patterns (such as a detached task):

    func fetchContacts() async throws -> [Contact] {
        let task = Task.detached {
            let keys = [
                CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
                CNContactEmailAddressesKey as CNKeyDescriptor
            ]
            let request = CNContactFetchRequest(keysToFetch: keys)
            let store = CNContactStore()
            let formatter = CNContactFormatter()
            formatter.style = .fullName
    
            var contacts: [Contact] = []
    
            try store.enumerateContacts(with: request) { contact, stop in
                guard !Task.isCancelled else {
                    stop.pointee = true
                    return
                }
    
                let contact = Contact(
                    givenName: contact.givenName,
                    familyName: contact.familyName,
                    fullName: formatter.string(from: contact),
                    emails: contact.emailAddresses.map { $0.value as String }
                )
                contacts.append(contact)
            }
    
            try Task.checkCancellation()
            return contacts
        }
    
        return try await withTaskCancellationHandler {
            try await task.value
        } onCancel: {
            task.cancel()
        }
    }
    

    In the above I also support cancelation by

    • exiting the enumeration if Task.isCancelled; and
    • throwing a CancellationError with Task.checkCancellation().

    Also, if you forgive me, but I introduced an additional parameter for the full name to your Contact type and use CNContactFormatter to build this string. That is a matter of personal preference, so do whatever you want in this regard. It is not relevant to the broader question.