1

I have a view model, that has multiple child view models. I am fairly new to watchOS, SwiftUI and Combine - taking this opportunity to learn.

I have a watchUI where it has

  1. Play Button (View) - SetTimerPlayPauseButton
  2. Text to show Time (View) - TimerText
  3. View Model - that has 1 WatchDayProgramViewModel - N: ExerciseTestClass - N: SetInformationTestClass. For each ExerciseSets, there is a watchTimer & watchTimerSubscription and I have managed to run the timer to update remaining rest time.
  4. ContentView - that has been injected the ViewModel as EnvironmentObject

If I tap SetTimerPlayPauseButton to start the timer, timer is running, working and changing the remainingRestTime(property within the child view model SetInformationTestClass) correctly, but the updates/changes are not being "published" to the TimerText View.

I have done most, if not all, the recommendation in other SO answers, I even made all my WatchDayProgramViewModel and ExerciseTestClass,SetInformationTestClass properties @Published, but they are still not updating the View, when the view model properties are updated as shown in the Xcode debugger below.

enter image description here

Please review my code and give me some advice on how to improve it.

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
        
    }
    
        
8
  • 1
    developer.apple.com/wwdc21/10009 Commented Jan 20, 2022 at 21:31
  • I have watched that series, any advice on which area I should be looking at? Commented Jan 21, 2022 at 4:42
  • 1
    All of it but especially the way they handle time. I personally think Timer is a bad idea, especially because it is unreliable. If the user has there arm down for “too long” or they switch to another app to let say play music or send a message your app is put in the background and the timer will fail. I choose to log start and end times instead or let HealthKit handle the time. But that is mostly my opinion a lot of people use Timer, I’ve just never found it reliable enough to log time with it. Commented Jan 21, 2022 at 14:24
  • Thanks for the valuable comment, I will keep that in mind, I mean, the watchApp was based on the workout app that Apple provided. Still, I want to figure out why my codes are not working, even if I would think about switching to TimelineSchedule and the way Apple did it. Commented Jan 21, 2022 at 14:45
  • 1
    It is more than likely not working because you are chaining ObservableObjects @Published will only detect a change when the object is changed as a whole now when variables change. One way to test is to wrap each SetInformationTestClass in an @ObservbleObject by using a subview that takes the object as a parameter. The other is so subscribe to SetInformationTestClass objectWillChange.send() using .sink. All this will allow you to refresh the view but it is an overcomplicated approach. Commented Jan 21, 2022 at 15:20

1 Answer 1

1

Managed to resolve the issue with help of @lorem ipsum and his feedback. As per his comment, the problem lied with the fact that

it is more than likely not working because you are chaining ObservableObjects @Published will only detect a change when the object is changed as a whole now when variables change. One way to test is to wrap each SetInformationTestClass in an @ObservbleObject by using a subview that takes the object as a parameter.

After which, I managed to find similar SO answers on changes in nested view model (esp child), and made the child view model an ObservedObject. The changes in child view model got populated to the view. Please see the changed code below.

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 ""
        }
    }
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.