web-dev-qa-db-ja.com

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

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

つまり、showErrorにBoolは必要ありません(ErrorオブジェクトがIdentifiableである必要があるだけです)

10
Ashley Mills

@ 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
        }
    }
}
3
Jacob Relkin