【问题标题】:async operations using Combine and SwiftUI使用 Combine 和 SwiftUI 的异步操作
【发布时间】:2020-06-10 02:00:34
【问题描述】:

我正在尝试弄清楚如何使用 Combine 和 SwiftUI 处理异步操作。

例如,我有一个 HealthKitManager 类,除其他外,它处理请求健康商店授权……

final class HealthKitManager {

    enum Error: Swift.Error {
        case notAvailable
        case authorisationError(Swift.Error)
    }

    let healthStore = HKHealthStore()

        func getHealthKitData(for objects: Set<HKObjectType>, completion: @escaping (Result<Bool, Error>) -> Void) {

        guard HKHealthStore.isHealthDataAvailable() else {
            completion(.failure(.notAvailable))
            return
        }

        self.healthStore.requestAuthorization(toShare: nil, read: objects) { completed, error in
            DispatchQueue.main.async {
                if let error = error {
                    completion(.failure(.authorisationError(error)))
                }
                completion(.success(completed))
            }
        }
    }
}

使用如下……

struct ContentView: View {

    let healthKitManager = HealthKitManager()

    @State var showNextView = false
    @State var showError = false
    @State var hkError: Error?

    let objectTypes = Set([HKObjectType.quantityType(forIdentifier: .bloodGlucose)!])

    var body: some View {
        NavigationView {
            NavigationLink(destination: NextView(), isActive: $showNextView) {
                Button("Show Next View") {
                    self.getHealthKitData()
                }
            }.navigationBarTitle("Content View")
        }.alert(isPresented: $showError) {
            Alert(title: Text("Error"), message: Text(hkError?.localizedDescription ?? ""), dismissButton: .cancel())
        }
    }

    func getHealthKitData() {
        self.healthKitManager.getHealthKitData(for: self.objectTypes) { result in
            switch result {
            case let .success(complete):
                self.showNextView = complete
            case let .failure(error):
                self.hkError = error
                self.showError = true
            }
        }
    }
}

我想做的是使用 Combine 而不是 Result 闭包。我猜是这样的……

final class HealthKitManager: ObservableObject {

    enum Error: Swift.Error {
        case notAvailable
        case authorisationError(Swift.Error)
    }

    @Published var authorisationResult: Result<Bool, Error>?

     let healthStore = HKHealthStore()

    func getHealthKitData(for objects: Set<HKObjectType>) {

        guard HKHealthStore.isHealthDataAvailable() else {
            self.authorisationResult = .failure(.notAvailable)
            return
        }

        self.healthStore.requestAuthorization(toShare: nil, read: objects) { completed, error in
            DispatchQueue.main.async {
                if let error = error {
                    self.authorisationResult = .failure(.authorisationError(error))
                    return
                }
                self.authorisationResult = .success(completed)
            }
        }
    }
}

但是不清楚如何绑定到NavigationLink(isActive:)alert(isPresented:) 的值,并得到错误。

struct ContentView: View {

    @ObservedObject var healthKitManager = HealthKitManager()

    let objectTypes = Set([HKObjectType.quantityType(forIdentifier: .bloodGlucose)!])

    var body: some View {
        NavigationView {
            NavigationLink(destination: NextView(), isActive: ????) { // How do I get this
                Button("Show Next View") {
                    self.healthKitManager.getHealthKitData(for: self.objectTypes)
                }
            }.navigationBarTitle("Content View")
        }.alert(isPresented: ????) { // or this
            Alert(title: Text("Error"), message: Text(????.localizedDescription ?? ""), dismissButton: .cancel()) // or this
        }
    }
}

我猜@Published var authorisationResult: Result&lt;Bool, Error&gt;? 不正确? 我应该使用Future / Promise 吗?


更新

我发现还有另一种显示警报的方式……

.alert(item: self.$error) { error in
        Alert(title: Text(error.localizedDescription))

这意味着我不需要 showError 的 Bool(它只需要 Error 对象为 Identifiable

【问题讨论】:

  • @Published 为您提供发布者,并通过 @ObservedObject 动态属性与 SwiftUI 视图刷新自动集成。您可以使用任何东西,但请考虑利弊。目标是让简单的事情变得复杂吗?

标签: asynchronous swiftui combine


【解决方案1】:

根据@Asperi's answer修改了我的答案:

extension Result {
    func getFailure() -> Failure? {
        switch self {
        case .failure(let er):
            return er
        default:
            return nil
        }
    }

    func binding<B>(
         success successClosure: (@escaping (Success) -> B),
         failure failureClosure: @escaping (Failure) -> B) -> Binding<B> {
        return Binding<B>(
        get: {
            switch self {
            case .success(let value):
                return successClosure(value)
            case .failure(let failure):
                return failureClosure(failure)
            }
        }, set: { _ in })
    }

    func implicitBinding(failure failureClosure: @escaping (Failure) -> Success) -> Binding<Success> {
        return binding(success: { $0 }, failure: failureClosure)
    }
}

class HealthKitManager: ObservableObject {
    enum Error: Swift.Error {
        case authorisationError(Swift.Error)
        case notAvailable
    }

    @Published var authorisationResult = Result<Bool, Error>.failure(.notAvailable)

    let healthStore = HKHealthStore()

    func getHealthKitData(for objects: Set<HKObjectType>) {
        guard HKHealthStore.isHealthDataAvailable() else {
            self.authorisationResult = .failure(.notAvailable)
            return
        }

        self.healthStore.requestAuthorization(toShare: nil, read: objects) { completed, error in
            DispatchQueue.main.async {
                if let error = error {
                    self.authorisationResult = .failure(.authorisationError(error))
                    return
                }

                self.authorisationResult = .success(completed)
            }
        }
    }
}

struct ContentView: View {
    @ObservedObject var healthKitManager = HealthKitManager()

    let objectTypes = Set([HKObjectType.quantityType(forIdentifier: .bloodGlucose)!])

    var body: some View {
        NavigationView {
            NavigationLink(destination: NextView(),
                           isActive: healthKitManager.authorisationResult.implicitBinding(failure: { _ in false })) {
                Button("Show Next View") {
                    self.healthKitManager.getHealthKitData(for: self.objectTypes)
                }
            }.navigationBarTitle("Content View")
        }.alert(isPresented: healthKitManager.authorisationResult.binding(success: { _ in false }, failure: { _ in true })) {
                let message = healthKitManager.authorisationResult.getFailure()?.localizedDescription ?? ""
                return Alert(title: Text("Error"), message: Text(message), dismissButton: .cancel()) // or this
        }
    }
}

【讨论】:

  • 谢谢。这肯定行得通,但是为hasAuthorizationErrorauthorizationErrorisAuthorized 设置不同的值似乎并不正确……尤其是当所有 3 个都被单个 Result 类型覆盖时。此类也可用于其他异步操作,因此为每个操作添加 3 个额外的 @Published 变量似乎很多。我希望Combine 有更好的方法来处理这个问题。
【解决方案2】:

我喜欢result,就像你在第二个变体中所做的那样

@Published var authorisationResult: Result<Bool, Error>?

所以可能的使用方法如下

NavigationLink(destination: NextView(), isActive: 
         Binding<Bool>.ifSuccess(self.healthKitManager.authorisationResult)) {
    Button("Show Next View") {
        self.healthKitManager.getHealthKitData(for: self.objectTypes)
    }
}.navigationBarTitle("Content View")

哪里有一些方便的扩展

extension Binding {
    static func ifSuccess<E>(_ result: Result<Bool, E>?) -> Binding<Bool> where E: Error {
        Binding<Bool>(
            get: {
                guard let result = result else { return false }
                switch result {
                 case .success(true):
                    return true
                 default:
                    return false
            }
        }, set: { _ in })
    }
}

error 的变体可以用类似的方式完成。

【讨论】:

  • 感谢您的回答 - 很遗憾,这需要额外的代码来执行此操作。
  • @AshleyMills,如果 Apple 为所有东西提供 API,我们会怎么做?我们不是程序员吗? =^)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-01-16
  • 2021-06-16
  • 2023-03-16
  • 2022-07-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多