EnvironmentObject 视图模型中的计时器不更新视图
Timer within EnvironmentObject view model not updating the View
我有一个视图模型,它有多个子视图模型。我是 watchOS、SwiftUI 和 Combine 的新手 - 借此机会学习。
我有一个 watchUI
- 播放按钮(视图)-
SetTimerPlayPauseButton
- 显示时间的文本(视图)-
TimerText
- 查看模型 - 有 1 个
WatchDayProgramViewModel
- N:ExerciseTestClass
- N:SetInformationTestClass
。对于每个 ExerciseSets,都有一个 watchTimer & watchTimerSubscription
,我已经设法 运行 计时器来更新剩余的休息时间。
- ContentView - 已将 ViewModel 作为
EnvironmentObject
注入
如果我点击 SetTimerPlayPauseButton
启动计时器,计时器正在 运行ning,正常工作并更改子视图模型 SetInformationTestClass
中的 remainingRestTime(属性) ,但是 updates/changes 没有“发布”到 TimerText
视图。
我已经完成了大部分(如果不是全部)其他 SO 答案中的建议,我什至完成了我所有的 WatchDayProgramViewModel
和 ExerciseTestClass
、SetInformationTestClass
属性 @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 ""
}
}
}
我有一个视图模型,它有多个子视图模型。我是 watchOS、SwiftUI 和 Combine 的新手 - 借此机会学习。
我有一个 watchUI
- 播放按钮(视图)-
SetTimerPlayPauseButton
- 显示时间的文本(视图)-
TimerText
- 查看模型 - 有 1 个
WatchDayProgramViewModel
- N:ExerciseTestClass
- N:SetInformationTestClass
。对于每个 ExerciseSets,都有一个watchTimer & watchTimerSubscription
,我已经设法 运行 计时器来更新剩余的休息时间。 - ContentView - 已将 ViewModel 作为
EnvironmentObject
注入
如果我点击 SetTimerPlayPauseButton
启动计时器,计时器正在 运行ning,正常工作并更改子视图模型 SetInformationTestClass
中的 remainingRestTime(属性) ,但是 updates/changes 没有“发布”到 TimerText
视图。
我已经完成了大部分(如果不是全部)其他 SO 答案中的建议,我什至完成了我所有的 WatchDayProgramViewModel
和 ExerciseTestClass
、SetInformationTestClass
属性 @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 ""
}
}
}