首页 > 解决方案 > 在 SwiftUI 中组合异步返回值

问题描述

我有来自 2 个不同类的 2 个异步返回值,一个来自 HealthKit,另一个来自 MotionManager。我的目标是组合这些值并将它们输出到 swiftui 视图中,它每秒刷新一次。我知道我必须在这里查看组合框架,但我不知道从哪里开始。我找不到很多描述 Swiftui + Combine 的教程。我知道我必须查看 .combineLatest 但我是否必须编写自己的发布者和订阅者,或者我可以使用我在这里拥有的@Published 属性包装器(@Published var motionData = MotionData() 和 @Published var heartRateValue: Double = 0.0 ) ?

我的 MotionManager 类:

struct MotionValues {
    var rotationX: Double = 0.0
    var rotationY: Double = 0.0
    var rotationZ: Double = 0.0
    var pitch: Double = 0.0
    var roll: Double = 0.0
    var yaw: Double = 0.0
}


class MotionManager: ObservableObject {

    @Published var motionValues = MotionValues()

    private let manager = CMMotionManager()

    func startMotionUpdates() {
        manager.deviceMotionUpdateInterval = 1.0
        manager.startDeviceMotionUpdates(to: .main) { (data, error) in

            guard let data = data, error == nil else {
                print(error!)
                return
            }

            self.motionValues.rotationX = data.rotationRate.x
            self.motionValues.rotationY = data.rotationRate.y
            self.motionValues.rotationZ = data.rotationRate.z

            self.motionValues.pitch = data.attitude.pitch
            self.motionValues.roll = data.attitude.roll
            self.motionValues.yaw = data.attitude.yaw
        }
    }

    func stopMotionUpdates() {
        manager.stopDeviceMotionUpdates()
        resetAllMotionData()
    }

    func resetAllMotionData() {
        self.motionValues.rotationX = 0.0
        self.motionValues.rotationY = 0.0
        self.motionValues.rotationZ = 0.0
        self.motionValues.pitch = 0.0
        self.motionValues.roll = 0.0
        self.motionValues.yaw = 0.0
    }
}

我的 HealthKitManager 类:

class HealthKitManager: ObservableObject {

    private var healthStore = HKHealthStore()
    private var heartRateQuantity = HKUnit(from: "count/min")
    private var activeQueries = [HKQuery]()

    @Published var heartRateValue: Double  = 0.0

    func autorizeHealthKit() {

        let heartRate = HKObjectType.quantityType(forIdentifier: .heartRate)!
        let heartRateVariability = HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN)!

        let HKreadTypes: Set = [heartRate, heartRateVariability]

        healthStore.requestAuthorization(toShare: nil, read: HKreadTypes) { (success, error) in
            if let error = error {
                print("Error requesting health kit authorization: \(error)")
            }
        }
    }

    func fetchHeartRateData(quantityTypeIdentifier: HKQuantityTypeIdentifier ) {

        let devicePredicate = HKQuery.predicateForObjects(from: [HKDevice.local()])
        let updateHandler: (HKAnchoredObjectQuery, [HKSample]?, [HKDeletedObject]?, HKQueryAnchor?, Error?) -> Void = {
        query, samples, deletedObjects, queryAnchor, error in
            guard let samples = samples as? [HKQuantitySample] else {
                return
            }
            self.process(samples, type: quantityTypeIdentifier)
        }
        let query = HKAnchoredObjectQuery(type: HKObjectType.quantityType(forIdentifier: quantityTypeIdentifier)!, predicate: devicePredicate, anchor: nil, limit: HKObjectQueryNoLimit, resultsHandler: updateHandler)
        query.updateHandler = updateHandler
        healthStore.execute(query)
        activeQueries.append(query)
    }

    private func process(_ samples: [HKQuantitySample], type: HKQuantityTypeIdentifier) {
        for sample in samples {
            if type == .heartRate {
                DispatchQueue.main.async {
                    self.heartRateValue = sample.quantity.doubleValue(for: self.heartRateQuantity)
                }
            }
        }
    }

    func stopFetchingHeartRateData() {
        activeQueries.forEach { healthStore.stop($0) }
        activeQueries.removeAll()
        DispatchQueue.main.async {
            self.heartRateValue = 0.0
        }

    }  
}

我开始创建combinedViewModel,但我被困在这里,不知道这是否是要走的路:

class CombinedViewModel: ObservableObject {

    @Published var motionManager: MotionManager = MotionManager()
    @Published var healthManager: HealthKitManager = HealthKitManager()

    var anyCancellable: AnyCancellable?

    init() {
        anyCancellable = Publishers
            .CombineLatest(motionManager.$motionValues,healthManager.$heartRateValue)
            .sink(receiveValue: {
                // Do something
            }
        })
    }
}

我需要把重点放在哪里?我是否需要完全学习组合框架来编写自己的发布者和订阅者,或者@Published 是否有可以完成这项工作的东西?或者我是否需要使用我的 CombinedViewModel 采用另一种方法?

添加 contentView 以供参考:

struct ContentView: View {

    @State var isActive: Bool = false

    private var motion = MotionManager()
    private var health = HealthKitManager()

    @ObservedObject var combinedViewModel = CombinedViewModel(managerOne: motion, managerTwo: health)

    private var motionValues: MotionValues {
        return combinedViewModel.combinedValues.0
    }

    private var heartRateValue: Double {
        return combinedViewModel.combinedValues.1
    }


    var body: some View {
        ScrollView {
            VStack(alignment: .leading) {
                Indicator(title: "X:", value: motionValues.rotationX)
                Indicator(title: "Y:", value: motionValues.rotationY)
                Indicator(title: "Z:", value: motionValues.rotationZ)
                Divider()
                Indicator(title: "Pitch:", value: motionValues.pitch)
                Indicator(title: "Roll:", value: motionValues.roll)
                Indicator(title: "Yaw:", value: motionValues.yaw)
                Divider()
                Indicator(title: "HR:", value: heartRateValue)
            }
            .padding(.horizontal, 10)
            Button(action: {
                self.isActive.toggle()
                self.isActive ? self.start() : self.stop()
            }) {
                Text(isActive ? "Stop" : "Start")
            }
            .background(isActive ? Color.green : Color.blue)
            .cornerRadius(10)
            .padding(.horizontal, 5)
        }.onAppear {
            self.health.autorizeHealthKit()
        }
    }

    private func start() {
        self.motion.startMotionUpdates()
        self.health.fetchHeartRateData(quantityTypeIdentifier: .heartRate)
    }

    private func stop() {
        self.motion.stopMotionUpdates()
        self.health.stopFetchingHeartRateData()
    }

}

标签: swiftuicombine

解决方案


您可以在您的中创建一个新的发布者(我会推荐一个AnyPublisher),CombinedViewModel它结合了两者的输出。这是您的代码的简化版本,带有CombinedViewModel

class ManagerOne {
  @Published var someValue = "Some Value"
}

class ManagerTwo {
  @Published var otherValue = "Other Value"
}

class CombinedViewModel {
  var combinedPublisher: AnyPublisher<(String, String), Never>

  init(managerOne: ManagerOne, managerTwo: ManagerTwo) {
    combinedPublisher = managerOne.$someValue
      .combineLatest(managerTwo.$otherValue)
      .eraseToAnyPublisher()
  }
}

如果您需要CombinedViewModel成为一个被观察的对象,您可以将代码修改为更像这样:

class CombinedViewModel: ObservableObject {
  @Published var combinedValue: (String, String) = ("", "")

  var cancellables = Set<AnyCancellable>()

  init(managerOne: ManagerOne, managerTwo: ManagerTwo) {
    managerOne.$someValue
      .combineLatest(managerTwo.$otherValue)
      .sink(receiveValue: { [weak self] combined  in
        self?.combinedValue = combined
      })
      .store(in: &cancellables)
  }
}

关于这一点的旁注:

@Published var motionManager: MotionManager = MotionManager()
@Published var healthManager: HealthKitManager = HealthKitManager()

由于这两个管理器都是类,$motionManager并且仅在您为它们分配新实例时$healthManager才会发出值。不是当任何一个经理的财产发生变化时。MotionManagerHealthKitManager


推荐阅读