asynchrone Operationen mit Combine und SwiftUI

8

Ich versuche herauszufinden, wie man mit asynchronen Operationen mit Combine und SwiftUI arbeitet.

Zum Beispiel habe ich eine HealthKitManagerKlasse, die unter anderem die Beantragung einer Genehmigung für ein Reformhaus behandelt…

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

welches wie folgt verwendet wird…

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

Was ich tun möchte, ist Kombinieren anstelle eines ResultVerschlusses. Ich vermute so etwas…

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

Aber dann ist es unklar, wie man sich an die Werte für NavigationLink(isActive:)und bindet alert(isPresented:)und den Fehler erhält.

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

Ich vermute, das @Published var authorisationResult: Result<Bool, Error>?ist nicht richtig? Soll ich Future / Promiseetwas anderes verwenden?


Aktualisieren

Ich habe festgestellt, dass es eine andere Möglichkeit gibt, eine Warnung anzuzeigen…

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

was bedeutet, dass ich den Bool nicht brauche showError(es muss nur das ErrorObjekt sein Identifiable)

Ashley Mills
quelle
@PublishedBietet Ihnen einen Publisher und verfügt über eine automatische Integration in die Aktualisierung der SwiftUI-Ansicht über @ObservedObjectdynamische Eigenschaften. Sie können alles verwenden, aber über Vor- und Nachteile nachdenken . Ist es das Ziel, einfache Dinge komplex zu machen?
Asperi

Antworten:

4

Ich mag es, resultwie du es in der zweiten Variante getan hast

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

Daher kann der mögliche Ansatz für die Verwendung wie folgt sein

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

wo eine bequeme Erweiterung

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

Die Variante für errorkann auf ähnliche Weise erfolgen.

Asperi
quelle
Vielen Dank für Ihre Antwort - es ist eine Schande, dass hierfür zusätzlicher Code erforderlich ist.
Ashley Mills
4
@AshleyMills, wenn Apple API für alles bereitstellen würde, was würden wir tun? Sind wir nicht Programmierer? = ^)
Asperi
3

Meine Antwort wurde überarbeitet, um auf der Antwort von @ Asperi zu basieren :

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
        }
    }
}
Jacob Relkin
quelle
1
Vielen Dank. Das würde definitiv funktionieren, aber separate Werte für hasAuthorizationErrorhaben authorizationErrorund isAuthorizedirgendwie nicht richtig erscheinen ... zumal alle 3 vom einzelnen Ergebnistyp abgedeckt werden. Diese Klasse kann auch für andere asynchrone Operationen verwendet werden, so dass das Hinzufügen von 3 zusätzlichen @PublishedVariablen für jede Operation viel zu sein scheint. Ich hatte gehofft, dass Combine eine bessere Möglichkeit haben würde, damit umzugehen.
Ashley Mills