4

I am creating a Contact Class to fetch user's phoneNumbers asynchronously.

I created 3 functions that leveraged on the new Combine framework's Future.

func checkContactsAccess() -> Future<Bool, Never>  {
    Future { resolve in
            let authorizationStatus = CNContactStore.authorizationStatus(for: .contacts)

        switch authorizationStatus {
            case .authorized:
                return resolve(.success(true))

            default:
                return resolve(.success(false))
        }
    }
}
func requestAccess() -> Future<Bool, Error>  {
    Future { resolve in
        CNContactStore().requestAccess(for: .contacts) { (access, error) in
            guard error == nil else {
                return resolve(.failure(error!))
            }

            return resolve(.success(access))
        }
    }
}
func fetchContacts() -> Future<[String], Error>  {
   Future { resolve in
            let contactStore = CNContactStore()
            let keysToFetch = [
                CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
                CNContactPhoneNumbersKey,
                CNContactEmailAddressesKey,
                CNContactThumbnailImageDataKey] as [Any]
            var allContainers: [CNContainer] = []

            do {
                allContainers = try contactStore.containers(matching: nil)
            } catch {
                return resolve(.failure(error))
            }

            var results: [CNContact] = []

            for container in allContainers {
                let fetchPredicate = CNContact.predicateForContactsInContainer(withIdentifier: container.identifier)

                do {
                    let containerResults = try contactStore.unifiedContacts(matching: fetchPredicate, keysToFetch: keysToFetch as! [CNKeyDescriptor])
                    results.append(contentsOf: containerResults)
                } catch {
                    return resolve(.failure(error))
                }
            }

            var phoneNumbers: [String] = []

            for contact in results {
                for phoneNumber in contact.phoneNumbers {
                    phoneNumbers.append(phoneNumber.value.stringValue.replacingOccurrences(of: " ", with: ""))
                }
            }

            return resolve(.success(phoneNumbers))
        }
}

Now how do I combine these 3 Future into a single future?

1) Check if permission is available

2) If true fetchContacts asynchronously

3) If false requestAccess asynchronously then fetchContacts asynchronously

Any tips or tricks of how you will handle this better are also welcomed

func getPhoneNumbers() -> Future<[String], Error> {
...
}

2 Answers2

10

Future is a Publisher. To chain Publishers, use .flatMap.

However, there is no need to chain futures in your use case, because there is only one asynchronous operation, namely the call to requestAccess. If you want to encapsulate the result of an operation that might throw an error, like your fetchContacts, what you want to return is not a Future but a Result.

To illustrate, I'll create a possible pipeline that does what you describe. Throughout the discussion, I'll first show some code, then discuss that code, in that order.

First, I'll prepare some methods we can call along the way:

func checkAccess() -> Result<Bool, Error> {
    Result<Bool, Error> {
        let status = CNContactStore.authorizationStatus(for:.contacts)
        switch status {
        case .authorized: return true
        case .notDetermined: return false
        default:
            enum NoPoint : Error { case userRefusedAuthorization }
            throw NoPoint.userRefusedAuthorization
        }
    }
}

In checkAccess, we look to see whether we have authorization. There are only two cases of interest; either we are authorized, in which case we can proceed to access our contacts, or we are not determined, in which case we can ask the user for authorization. The other possibilities are of no interest: we know we have no authorization and we cannot request it. So I characterize the result, as I said earlier, as a Result:

  • .success(true) means we have authorization

  • .success(false) means we don't have authorization but we can ask for it

  • .failure means don't have authorization and there is no point going on; I make this a custom Error so we can throw it in our pipeline and thus complete the pipeline prematurely.

OK, on to the next function.

func requestAccessFuture() -> Future<Bool, Error> {
    Future<Bool, Error> { promise in
        CNContactStore().requestAccess(for:.contacts) { ok, err in
            if err != nil {
                promise(.failure(err!))
            } else {
                promise(.success(ok)) // will be true
            }
        }
    }
}

requestAccessFuture embodies the only asynchronous operation, namely requesting access from the user. So I generate a Future. There are only two possibilities: either we will get an error or we will get a Bool that is true. There are no circumstances under which we get no error but a false Bool. So I either call the promise's failure with the error or I call its success with the Bool, which I happen to know will always be true.

func getMyEmailAddresses() -> Result<[CNLabeledValue<NSString>], Error> {
    Result<[CNLabeledValue<NSString>], Error> {
        let pred = CNContact.predicateForContacts(matchingName:"John Appleseed")
        let jas = try CNContactStore().unifiedContacts(matching:pred, keysToFetch: [
            CNContactFamilyNameKey as CNKeyDescriptor, 
            CNContactGivenNameKey as CNKeyDescriptor, 
            CNContactEmailAddressesKey as CNKeyDescriptor
        ])
        guard let ja = jas.first else {
            enum NotFound : Error { case oops }
            throw NotFound.oops
        }
        return ja.emailAddresses
    }
}

getMyEmailAddresses is just a sample operation accessing the contacts. Such an operation can throw, so I express it once again as a Result.

Okay, now we're ready to build the pipeline! Here we go.

self.checkAccess().publisher

Our call to checkAccess yields a Result. But a Result has a publisher! So that publisher is the start of our chain. If the Result didn't get an error, this publisher will emit a Bool value. If it did get an error, the publisher will throw it down the pipeline.

.flatMap { (gotAccess:Bool) -> AnyPublisher<Bool, Error> in
    if gotAccess {
        let just = Just(true).setFailureType(to:Error.self).eraseToAnyPublisher()
        return just
    } else {
        let req = self.requestAccessFuture().eraseToAnyPublisher()
        return req
    }
}

This is the only interesting step along the pipeline. We receive a Bool. If it is true, we have no work to do; but if it is false, we need to get our Future and publish it. The way you publish a publisher is with .flatMap; so if gotAccess is false, we fetch our Future and return it. But what if gotAccess is true? We still have to return a publisher, and it needs to be of the same type as our Future. It doesn't actually have to be a Future, because we can erase to AnyPublisher. But it must be of the same types, namely Bool and Error.

So we create a Just and return it. In particular, we return Just(true), to indicate that we are authorized. But we have to jump through some hoops to map the error type to Error, because a Just's error type is Never. I do that by applying setFailureType(to:).

Okay, the rest is easy.

.receive(on: DispatchQueue.global(qos: .userInitiated))

We jump onto a background thread, so that we can talk to the contact store without blocking the main thread.

.compactMap { (auth:Bool) -> Result<[CNLabeledValue<NSString>], Error>? in
    if auth {
        return self.getMyEmailAddresses()
    }
    return nil
}

If we receive true at this point, we are authorized, so we call getMyEmailAddress and return the result, which, you recall, is a Result. If we receive false, we want to do nothing; but we are not allowed to return nothing from map, so we use compactMap instead, which allows us to return nil to mean "do nothing". Therefore, if we got an error instead of a Bool, the error will just pass on down the pipeline unchanged.

.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
    if case let .failure(err) = completion {
        print("error:", err)
    }
}, receiveValue: { result in
    if case let .success(emails) = result {
        print("got emails:", emails)
    }
})

We've finished, so it remains only to get ready to receive the error or the emails (wrapped in a Result) that have come down the pipeline. I do this, by way of illustration, simply by getting back onto the main thread and printing out what comes down the pipeline at us.


This description doesn't seem quite enough to give some readers the idea, so I've posted an actual example project at https://github.com/mattneub/CombineAuthorization.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Hey matt thanks for your answer. The reason I use future for accessing contact is to run that piece of code asynchronously. But receiving in a different thread works as well. For the error, I believe there is a `.setFailureType(to: Error.self)` that is able to do just that. – kelvinleeweisern Feb 27 '20 at 03:08
  • Cool, I'll try that, thanks! — Yup, perfect, I'll change that part of the code. – matt Feb 27 '20 at 03:31
  • Well, my point is, merely making something a Future doesn't magically make it asynchronous in any useful way; and in particular it doesn't help you get onto a background thread, which is _necessary_ when you talk to the contacts store. I hope the example has shown you how `flatMap` helps chain publishers, which seems to be the part of the puzzle you were puzzling over. You'll notice too that my answer works smoothly in all situations: we are already authorized; we are not authorized but we can ask for authorization; and, we are not authorized and we can't ask for authorization. – matt Feb 27 '20 at 03:36
-1

You can use this framework for Swift coroutines - https://github.com/belozierov/SwiftCoroutine

When you call await it doesn’t block the thread but only suspends coroutine, so you can use it in the main thread as well.

DispatchQueue.main.startCoroutine {
    let future = checkContactsAccess()
    let coFuture = future.subscribeCoFuture()
    let success = try coFuture.await()

}
Alex Belozierov
  • 131
  • 1
  • 4