swift 是否有办法在单独的类中使用@Published?

wyyhbhjk  于 2023-05-16  发布在  Swift
关注(0)|答案(1)|浏览(125)

我是Swift的新手,我试图创建一个锻炼WatchApp,我有2个页面,初始页面将显示最后一个活动和一个开始锻炼的按钮,我有另一个页面将显示当前的锻炼。
我用NSObject, HKLiveWorkoutBuilderDelegate, HKWorkoutSessionDelegate, ObservableObject创建了一个WorkoutSession类,并获取我需要的属性,当点击开始锻炼时,将调用一个名为startWorkout的函数开始收集数据,下面的代码:
ContentView(主视图):

import SwiftUI
import HealthKit

struct ContentView: View {
    var body: some View {
        NavigationView {
            VStack(alignment: .leading) {
                VStack(alignment: .leading) {
                    Text("Última atividade")
                        .padding(.bottom, 12)
                    
                    HStack {
                        
                        CaloriesRingView(
                            calories: 168
                        )
                        .padding(.trailing, 24)
                        
                        DurationRingView(
                            duration: 5400
                        )
                    }
                    
                    
                }.padding(.top, 12)
                
                NavigationLink(destination: WorkoutView()) {
                    Text("Começar treino")
                }
                .background(.green)
                .cornerRadius(20)
                .padding(.top, 24)
            }
            .padding(.horizontal, 12)
        }
        .navigationBarBackButtonHidden(true)
    }
}

训练视图:

import SwiftUI

struct WorkoutView: View {
    @ObservedObject var workoutSession = WorkoutSession()
    
    init() {
        workoutSession.setUpSession()
        workoutSession.startWorkoutSession()
    }
    
    var body: some View {
        NavigationView {
            if workoutSession.status == .inProgress {
                if workoutSession.bpm != nil {
                    Text(heartBeatFormatter())
                }
                
                if workoutSession.energyBurned != nil {
                    Text("\(workoutSession.energyBurned!) kcal")
                }
                
                if workoutSession.elapsedTime != nil {
                    Text("\(workoutSession.elapsedTime!)")
                }
            }
        }
        .navigationBarBackButtonHidden(true)

    }
    
    func heartBeatFormatter() -> String {
        let formatter = NumberFormatter()
        formatter.maximumSignificantDigits = 0
        return formatter.string(from: workoutSession.bpm! as NSNumber) ?? ""
    }
}

我的WorkoutSession:

import Foundation
import HealthKit
import SwiftUI

enum WorkoutSessionStatus {
    case inProgress, complete, cancelled, notStarted, paused
}

class WorkoutSession: NSObject, HKLiveWorkoutBuilderDelegate, HKWorkoutSessionDelegate, ObservableObject {
    @Published var energyBurnedStatistics: HKStatistics?;
    @Published var heartRateStatistics: HKStatistics?;
    @Published var elapsedTimeStatistics: HKStatistics?;
    
    @Published var energyBurned: Double?;
    @Published var bpm: Double?;
    @Published var elapsedTime: TimeInterval?;
    @Published var status = WorkoutSessionStatus.notStarted;
    @Published var workoutData: HKWorkout?
    
    @Published var session: HKWorkoutSession?;
    @Published var builder: HKLiveWorkoutBuilder?;
    
    var healthStore = HKHealthStore();
    
    func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) {
        self.heartRateStatistics = workoutBuilder.statistics(for: .init(.heartRate))
        
        self.energyBurned = workoutBuilder.statistics(for: .init(.activeEnergyBurned))?.sumQuantity()?.doubleValue(for: .kilocalorie())
        self.energyBurnedStatistics = workoutBuilder.statistics(for: .init(.activeEnergyBurned))
        
        self.elapsedTime = workoutBuilder.elapsedTime
        
        self.bpm = calculateBPM()
    }
    
    func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) {
        
    }
    
    func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState, from fromState: HKWorkoutSessionState, date: Date) {
    
    }
    
    func workoutSession(_ workoutSession: HKWorkoutSession, didFailWithError error: Error) {
        
    }
    
    func setUpSession() {
        let typesToShare: Set<HKWorkoutType> = [HKQuantityType.workoutType()]
        
        let typesToRead: Set<HKQuantityType> = [
            HKQuantityType.quantityType(forIdentifier: .heartRate)!,
            HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!,
        ]
        
        healthStore.requestAuthorization(toShare: typesToShare, read: typesToRead) { (success, error) in
            if !success {
//                TODO: Levar para uma página dizendo que só pode usar o app após autorizar
            }
        }
    }
    
    func startWorkoutSession() {
        let configuration = HKWorkoutConfiguration()
        configuration.activityType = .functionalStrengthTraining
        configuration.locationType = .indoor
        
        do {
            session = try HKWorkoutSession(healthStore: healthStore, configuration: configuration)
            builder = session!.associatedWorkoutBuilder()
            
            builder!.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore, workoutConfiguration: configuration)
            session!.delegate = self
            builder!.delegate = self
            session!.startActivity(with: Date())
            builder!.beginCollection(withStart: Date()) { success, error in
                if !success {
                    print("Unable to start collection, the error: \(String(describing: error))")
                    return
                }
                
                self.status = .inProgress
            }
        } catch {
            print("Unable to create workout session, the error: \(String(describing: error))")
        }
    }
    
    func endWorkoutSession() {
        guard let session = session else {
            print("Session is nil. Unable to end workout.")
            return
        }
        
        guard let builder = builder else {
            print("Builder is nil. Unable to end workout.")
            return
        }
        
        session.end()
        
        builder.endCollection(withEnd: Date()) { success, error in
            if !success {
                print("Unable to end collection")
                return
            }
            
            builder.finishWorkout { workout, error in
                if workout == nil {
                    print("Unable to read workout")
                    return
                }
                
                self.status = .complete
                self.workoutData = workout
            }
        }
    }
    
    func resumeWorkout() {
        guard let session = session else {
            print("Session is nil. Unable to end workout.")
            return
        }
        
        session.resume()
        self.status = .inProgress
    }
    
    func pauseWorkout() {
        guard let session = session else {
            print("Session is nil. Unable to end workout.")
            return
        }
        
        session.pause()
        self.status = .paused
    }
}

extension WorkoutSession {
    private func calculateBPM() -> Double? {
        let countUnit: HKUnit = .count()
        let minuteUnit: HKUnit = .minute()
        let beatsPerMinute: HKUnit = countUnit.unitDivided(by: minuteUnit)
        
        return self.heartRateStatistics?.mostRecentQuantity()?.doubleValue(for: beatsPerMinute)
    }
}

当我调用workoutSession.startWorkoutSession()函数时,我得到了一些错误:

2023-05-09 18:12:14.741560-0300 ApodWatch Watch App[67831:629899] [SwiftUI] Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.

我已经尝试更改为@State@StateObject但不起作用,我的期望是与@Published...

vql8enpb

vql8enpb1#

尝试像这样进行重组:

struct WorkoutView: View {
    @StateObject var workoutSession = WorkoutSession() // the init doesn't actually happen here
    
/* move this to WorkoutSession's init
    init() {
        workoutSession.setUpSession()
        workoutSession.startWorkoutSession()
    }
*/
     
    // @StateObject will init `WorkoutSession` just before body is called, which is just before its underlying UIView appears on screen.
    var body: some View {

另外,您最好有一个View@StateObject对用于授权。然后在该视图的身体检查,如果授权和初始化另一个View和其他@StateObject的锻炼。
在Apple的Build a workout app for Apple Watch示例中,他们在workoutSession委托方法中使用DispatchQueue.main.async来防止[SwiftUI] Publishing changes from background threads is not allowed。不幸的是,这个例子没有授权失败时的错误处理,所以我仍然建议将授权和训练分离到不同的对象和视图中。

相关问题