J'essaie de comprendre comment travailler avec des opérations asynchrones en utilisant Combine et SwiftUI.
Par exemple, j'ai une classe HealthKitManager
qui, entre autres, gère la demande d'autorisation de magasin de santé…
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))
}
}
}
}
qui est utilisé comme suit…
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
}
}
}
}
Ce que j'aimerais faire, c'est utiliser Combine plutôt qu'une fermeture Result
. Je suppose que quelque chose comme ça ...
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)
}
}
}
}
Mais alors on ne sait pas comment se lier aux valeurs de NavigationLink(isActive:)
et alert(isPresented:)
, et pour obtenir l'erreur.
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
}
}
}
Je suppose que @Published var authorisationResult: Result<Bool, Error>?
N'est pas correct? Dois-je utiliser Future / Promise
, Autre chose?
Mettre à jour
J'ai trouvé qu'il existe une autre façon de présenter une alerte…
.alert(item: self.$error) { error in
Alert(title: Text(error.localizedDescription))
ce qui signifie que je n'ai pas besoin du Bool pour showError
(il faut juste que l'objet Error
soit Identifiable
)
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
}
}
}