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
.
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
Task.isCancelled
; andCancellationError
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.