【问题标题】:Using Combine's Future to replicate async await in Swift使用 Combine 的 Future 在 Swift 中复制异步等待
【发布时间】:2020-02-26 14:49:45
【问题描述】:

我正在创建一个联系人类来异步获取用户的电话号码。

我利用新的 Combine 框架的 Future 创建了 3 个函数。

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))
        }
}

现在如何将这 3 个 Future 组合成一个 Future?

1) 检查权限是否可用

2) 如果为 true 则异步 fetchContacts

3) 如果为 false requestAccess 异步则 fetchContacts 异步

也欢迎任何有关如何更好地处理此问题的提示或技巧

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

【问题讨论】:

  • 请显示实际代码,而不是三个省略号。
  • 我用实际代码更新了@matt
  • 你应该打电话给resolve。不要返回调用它的“结果”。到处删除return
  • 您的授权状态代码也是错误的。然后您继续请求授权的唯一情况是.notDetermined,但您没有区分这种情况。
  • 同样调用 authorizationStatus(for:) 不是异步的,因此没有必要从中创建 Future。

标签: swift asynchronous async-await combine


【解决方案1】:

未来是出版商。要链接发布者,请使用.flatMap

但是,在您的用例中不需要链接期货,因为只有一个异步操作,即对requestAccess 的调用。如果你想封装一个可能引发错误的操作的结果,比如你的fetchContacts,你想返回的不是Future而是Result。

为了说明,我将创建一个可能的管道来执行您所描述的操作。在整个讨论过程中,我将首先展示一些代码,然后按顺序讨论该代码。

首先,我会准备一些我们可以沿途调用的方法:

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
        }
    }
}

checkAccess,我们看看是否有授权。只有两个感兴趣的案例;要么我们被授权,在这种情况下我们可以继续访问我们的联系人,或者我们不确定,在这种情况下我们可以要求用户授权。其他可能性是不感兴趣的:我们知道我们没有授权,我们不能请求它。因此,正如我之前所说,我将结果描述为结果:

  • .success(true) 表示我们有授权

  • .success(false) 表示我们没有授权,但我们可以要求它

  • .failure 表示没有授权,没有意义;我将此设置为自定义错误,以便我们可以将其放入管道中,从而过早地完成管道。

好的,进入下一个功能。

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体现了唯一的异步操作,即请求用户访问。所以我产生了一个未来。只有两种可能性:要么我们会得到一个错误,要么我们会得到一个布尔值true。除了false Bool,没有任何情况下我们不会出错。所以我要么用错误来调用promise的失败,要么用Bool来调用它的成功,我碰巧知道它总是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 只是一个访问联系人的示例操作。这样的操作可以抛出,所以我再次表示为Result。

好的,现在我们已经准备好构建管道了!我们开始吧。

self.checkAccess().publisher

我们对checkAccess 的调用会产生一个结果。但是一个结果有一个发布者!所以那个出版商是我们链条的开始。如果 Result 没有收到错误,此发布者将发出一个 Bool 值。如果它确实出现错误,发布者会将其丢弃在管道中。

.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
    }
}

这是整个流程中唯一有趣的一步。我们收到一个布尔值。如果是真的,我们没有工作可做;但如果它是错误的,我们需要获取我们的 Future 并发布它。您发布发布者的方式是使用.flatMap;所以如果gotAccess 是假的,我们获取我们的Future 并返回它。但是如果gotAccess 是真的呢?我们仍然必须返回一个发布者,并且它需要与我们的 Future 具有相同的类型。它实际上不必成为 Future,因为我们可以擦除到 AnyPublisher。但它必须是相同的类型,即Bool和Error。

所以我们创建一个 Just 并返回它。特别是,我们返回Just(true),表示我们已获得授权。但是我们必须跳过一些障碍来将错误类型映射到 Error,因为 Just 的错误类型是 Never。我通过申请 setFailureType(to:) 来做到这一点。

好的,剩下的就简单了。

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

我们跳到后台线程,这样我们就可以在不阻塞主线程的情况下与联系人存储进行对话。

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

如果此时我们收到true,则我们已获得授权,因此我们调用getMyEmailAddress 并返回结果,您还记得,它是一个结果。如果我们收到false,我们什么也不想做;但是我们不允许从map 返回任何内容,因此我们使用compactMap 代替,这允许我们返回nil 来表示“什么都不做”。因此,如果我们得到一个错误而不是 Bool,则该错误将原封不动地传递到管道中。

.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)
    }
})

我们已经完成了,所以剩下的只是准备接收错误或通过管道发送的电子邮件(包装在结果中)。为了说明这一点,我只需回到主线程并打印出我们的管道。


这个描述似乎不足以让一些读者有所了解,所以我在https://github.com/mattneub/CombineAuthorization 发布了一个实际的示例项目。

【讨论】:

  • 嗨,马特,感谢您的回答。我使用 future 访问联系人的原因是异步运行那段代码。但是在不同的线程中接收也可以。对于这个错误,我相信有一个.setFailureType(to: Error.self) 能够做到这一点。
  • 酷,我试试,谢谢! — 是的,完美,我将更改那部分代码。
  • 好吧,我的意思是,仅仅将某些东西做成 Future 并不能神奇地让它以任何有用的方式异步;特别是它不能帮助您进入后台线程,当您与联系人存储交谈时,这是必要的。我希望该示例向您展示了flatMap 如何帮助连锁出版商,这似乎是您困惑的难题的一部分。你也会注意到我的回答在所有情况下都很顺利:我们已经获得授权;我们没有被授权,但我们可以请求授权;而且,我们没有得到授权,也不能请求授权。
【解决方案2】:

您可以将此框架用于 Swift 协程 - https://github.com/belozierov/SwiftCoroutine

当你调用 await 时,它不会阻塞线程,只会挂起协程,所以你也可以在主线程中使用它。

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

}

【讨论】:

    猜你喜欢
    • 2022-08-04
    • 1970-01-01
    • 1970-01-01
    • 2017-09-30
    • 1970-01-01
    • 2017-04-16
    • 1970-01-01
    • 2018-04-11
    • 2021-10-13
    相关资源
    最近更新 更多