EnvironmentObject 视图模型中的计时器不更新视图

Timer within EnvironmentObject view model not updating the View

我有一个视图模型,它有多个子视图模型。我是 watchOS、SwiftUI 和 Combine 的新手 - 借此机会学习。

我有一个 watchUI

  1. 播放按钮(视图)- SetTimerPlayPauseButton
  2. 显示时间的文本(视图)- TimerText
  3. 查看模型 - 有 1 个 WatchDayProgramViewModel - N:ExerciseTestClass - N:SetInformationTestClass。对于每个 ExerciseSets,都有一个 watchTimer & watchTimerSubscription,我已经设法 运行 计时器来更新剩余的休息时间。
  4. ContentView - 已将 ViewModel 作为 EnvironmentObject
  5. 注入

如果我点击 SetTimerPlayPauseButton 启动计时器,计时器正在 运行ning,正常工作并更改子视图模型 SetInformationTestClass 中的 remainingRestTime(属性) ,但是 updates/changes 没有“发布”到 TimerText 视图

我已经完成了大部分(如果不是全部)其他 SO 答案中的建议,我什至完成了我所有的 WatchDayProgramViewModelExerciseTestClassSetInformationTestClass 属性 @Published,但他们仍然没有更新视图,当视图模型属性更新时如下面的 Xcode 调试器所示。

请检查我的代码并给我一些改进建议。

ContentView

struct ContentView: View {
    @State var selectedTab = 0
    @StateObject var watchDayProgramVM = WatchDayProgramViewModel()
    
    var body: some View {
        
        TabView(selection: $selectedTab) {
            
            SetRestDetailView().id(2)
    
        }
        .environmentObject(watchDayProgramVM)
        .tabViewStyle(PageTabViewStyle())
        .indexViewStyle(.page(backgroundDisplayMode: .automatic))
        
    }
}

    
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            ContentView(watchDayProgramVM: WatchDayProgramViewModel())
        }
    }
}

SetRestDetailView

import Foundation
import SwiftUI
import Combine

struct SetRestDetailView: View {
    
    @EnvironmentObject var watchDayProgramVM: WatchDayProgramViewModel
    
    var setCurrentHeartRate: Int = 120
    @State var showingLog = false
    
    
    var body: some View {


                    HStack {
         
                        let elapsedRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].elapsedRestTime
                        let totalRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].totalRestTime
                        
                        TimerText(elapsedRestTime: elapsedRestTime, totalRestTime: totalRestTime, rect: rect)
                            .border(Color.yellow)

                    }
                    
                    HStack {
                        
                        SetTimerPlayPauseButton(isSetTimerRunningFlag: false,
                                                playImage: "play.fill",
                                                pauseImage: "pause.fill",
                                                bgColor: Color.clear,
                                                fgColor: Color.white.opacity(0.5),
                                                rect: rect) {
                            
                            print("playtimer button tapped")
                            self.watchDayProgramVM.exerciseVMList[0].sets[2].startTimer()
                            
                            
                            let elapsedRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].elapsedRestTime
                            let totalRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].totalRestTime
                            print("printing elapsedRestTime from SetRestDetailView \(elapsedRestTime)")
                            print("printing elapsedRestTime from SetRestDetailView \(totalRestTime)")
                            
                        }
                            .border(Color.yellow)

                    }

 }

}

TimerText

struct TimerText: View {
    var elapsedRestTime: Int
    var totalRestTime: Int
    var rect: CGRect
    
    var body: some View {
        VStack {
            Text(counterToMinutes())
                .font(.system(size: 100, weight: .semibold, design: .rounded))
                .kerning(0)
                .fontWeight(.semibold)
                .minimumScaleFactor(0.25)
                .padding(-1)
        }
    }
    
    func counterToMinutes() -> String {
        let currentTime = totalRestTime - elapsedRestTime
        let seconds = currentTime % 60
        let minutes = Int(currentTime / 60)
        
        if currentTime > 0 {
            return String(format: "%02d:%02d", minutes, seconds)
        }
        
        else {
            return ""
        }
    }
}

ViewModel

import Combine

final class WatchDayProgramViewModel: ObservableObject {
    
    @Published var exerciseVMList: [ExerciseTestClass] = [
 (static/hard-coded values for testing)
]

class ExerciseTestClass: ObservableObject {
    
    init(exercise: String, sets: [SetInformationTestClass]) {
        
        self.exercise = exercise
        self.sets = sets
        
    }
    
        var exercise: String
        @Published var sets: [SetInformationTestClass]
    
    }

class SetInformationTestClass: ObservableObject {
    
    init(totalRestTime: Int, elapsedRestTime: Int, remainingRestTime: Int, isTimerRunning: Bool) {
        
        self.totalRestTime = totalRestTime
        self.elapsedRestTime = elapsedRestTime
        self.remainingRestTime = remainingRestTime
        self.isTimerRunning = isTimerRunning
        
    }
    
    @Published var totalRestTime: Int
    @Published var elapsedRestTime: Int
    @Published var remainingRestTime: Int
    
    @Published var isTimerRunning = false
    @Published var watchTimer = Timer.publish(every: 1.0, on: .main, in: .default)
    @Published var watchTimerSubscription: AnyCancellable? = nil
    
    @Published private var startTime: Date? = nil
    
    
    func startTimer() {
        
        print("startTimer initiated")
        self.watchTimerSubscription?.cancel()
        
        if startTime == nil {
            startTime = Date()
        }
        
        self.isTimerRunning = true
        
        self.watchTimerSubscription = watchTimer
            .autoconnect()
            .sink(receiveValue: { [weak self] _ in
                
                guard let self = self, let startTime = self.startTime else { return }
                
                let now = Date()
                let elapsedTime = now.timeIntervalSince(startTime)
                
                self.remainingRestTime = self.totalRestTime - Int(elapsedTime)
                
                self.elapsedRestTime = self.totalRestTime - self.remainingRestTime
                                                    
                guard self.remainingRestTime > 0 else {
                        self.pauseTimer()
                        return
                    }

self.objectWillChange.send()
                
                print("printing elapsedRest Time \(self.elapsedRestTime) sec")
                print("printing remaining Rest time\(self.remainingRestTime)sec ")
                
            })
    }
    
    func pauseTimer() {
        //stop timer and retain elapsed rest time
        
        print("pauseTimer initiated")
        self.watchTimerSubscription?.cancel()
        self.watchTimerSubscription = nil
        self.isTimerRunning = false
        self.startTime = nil
        
    }
    
        

在@lorem ipsum 和他的反馈的帮助下设法解决了这个问题。根据他的评论,问题在于

它很可能不起作用,因为您正在链接 ObservableObjects @Published 现在,当变量发生变化时,对象作为一个整体发生变化时,只会检测到变化。一种测试方法是使用将对象作为参数的子视图将每个 SetInformationTestClass 包装在 @ObservbleObject 中。

之后,我设法找到了关于嵌套视图模型(尤其是子视图)更改的类似 SO 答案,并将子视图模型设为 ObservedObject。子视图模型中的更改已填充到视图中。请查看下面更改后的代码。

SetRestDetailView

import Foundation
import SwiftUI
import Combine

struct SetRestDetailView: View {
    
    @EnvironmentObject var watchDayProgramVM: WatchDayProgramViewModel
    
    var setCurrentHeartRate: Int = 120
    @State var showingLog = false
    
    
    var body: some View {


                    HStack {
         
                        let elapsedRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].elapsedRestTime
                        let totalRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].totalRestTime
                        
                        let setInformatationVM = self.watchDayProgramVM.exerciseVMList[0].sets[2]
                        
                        TimerText(setInformationVM: setInformatationVM, rect: rect)
                            .border(Color.yellow)

                    }
                    
                    HStack {
                        
                        SetTimerPlayPauseButton(isSetTimerRunningFlag: false,
                                                playImage: "play.fill",
                                                pauseImage: "pause.fill",
                                                bgColor: Color.clear,
                                                fgColor: Color.white.opacity(0.5),
                                                rect: rect) {
                            
                            print("playtimer button tapped")
                            self.watchDayProgramVM.exerciseVMList[0].sets[2].startTimer()
                            
                            
                            let elapsedRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].elapsedRestTime
                            let totalRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].totalRestTime
                            print("printing elapsedRestTime from SetRestDetailView \(elapsedRestTime)")
                            print("printing elapsedRestTime from SetRestDetailView \(totalRestTime)")
                            
                        }
                            .border(Color.yellow)

                    }

 }

}

TimerText

struct TimerText: View {
    
    @ObservedObject var setInformationVM: SetInformationTestClass
    
//    @State var elapsedRestTime: Int
//    @State var totalRestTime: Int
    var rect: CGRect
    
    var body: some View {
        VStack {
            Text(counterToMinutes())
                .font(.system(size: 100, weight: .semibold, design: .rounded))
                .kerning(0)
                .fontWeight(.semibold)
                .minimumScaleFactor(0.25)
                .padding(-1)
        }
    }
    
    func counterToMinutes() -> String {
        let currentTime = setInformationVM.totalRestTime - setInformationVM.elapsedRestTime
        let seconds = currentTime % 60
        let minutes = Int(currentTime / 60)
        
        if currentTime > 0 {
            return String(format: "%02d:%02d", minutes, seconds)
        }
        
        else {
            return ""
        }
    }
}