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.