CombineとSwiftUIを使用した非同期操作


8

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

私がやりたいことは、ResultクロージャーではなくCombineを使用することです。私はこのようなものを推測しています...

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<Bool, Error>?は正しくないと思いますか?Future / Promise、他に何かを使用する必要がありますか?


更新

警告を表示する別の方法があることを発見しました…

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

これは、ブール値が必要ないことを意味しますshowErrorErrorオブジェクトがである必要があるだけですIdentifiable


@Publishedパブリッシャーを提供し、@ObservedObject動的プロパティを介してSwiftUIビューの更新と自動的に統合します。何でも使用できますが、賛否両論について考えてください。単純なものを複雑にすることが目標ですか?
Asperi

回答:


4

resultはあなたが2番目のバリアントでしたように持っているのが好き

@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、同様の方法で実行できます。


あなたの答えをありがとう-これを行うにはそのような追加のコードが必要なのは残念です。
アシュレイミルズ

4
@AshleyMills、もしAppleがすべてのAPIを提供したら、どうしますか?プログラマーではないでしょうか。= ^)
Asperi

3

@Asperiの答えに基づいて私の答えを修正しました:

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

1
ありがとうございました。それは仕事間違いだろうが、のために別々の値を持つhasAuthorizationErrorauthorizationErrorそしてisAuthorized右何とか...特に、すべての3のような単一の結果のタイプによって覆われていないようです。また、このクラスは他の非同期操作に使用される可能性がある@Publishedため、各操作に3つの追加の変数を追加することは非常に困難です。Combineがこれを処理するより良い方法を持っていることを望んでいました。
アシュレイミルズ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.