Search code examples
iosswiftswiftuitimerobservedobject

SwiftUI Timer keep resetting when updating the ObservedObjects


I just made a simple code of a timer, and am trying to figure out a way to NOT reset the timer when other observed object status is changed. When I start the timer with init function, it resets whenever other observed objects' status is changed. When I start the timer with onAppear, it gets changed once other observed objects' status is changed and never start again. What I want to accomplish is, the timer starts once and doesn't reset when other observed objects have changed during other observed objects are passed out from other View and the tiemr itself has to be Subview. Any suggestions?

import SwiftUI
import Combine
import Foundation

struct ContentView: View {

    @ObservedObject var apptCardVM: ApptCardViewModel
    @ObservedObject var timerData = TimerDataViewModel()

    var body: some View {
        VStack {
            CurrentDateView(timerData: timerData)  // << here !!

            Picker("Seizure Type", selection: $apptCardVM.typeIndex) {
            ForEach(0..<apptCardVM.typeChoice.count) {
                Text(self.apptCardVM.typeChoice[$0])
            }
        }.pickerStyle(SegmentedPickerStyle())

        }

    }
}

struct CurrentDateView: View {
    @State private var currentDate = Date()
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    @ObservedObject var timerData: TimerDataViewModel
    var body: some View {

        Text("\(Int(timerData.hoursElapsed), specifier: "%02d"):\(Int(timerData.minutesElapsed), specifier: "%02d"):\(Int(timerData.secondsElapsed), specifier: "%02d")")
            .fontWeight(.bold)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .onAppear(){
                timerData.start()
            }
    }
}

class ApptCardViewModel: ObservableObject, Identifiable {
    @Published var typeChoice = ["Quick", "Long", "FullService"]
    @Published var typeIndex: Int = 0
    private var cancellables = Set<AnyCancellable>()


}

class TimerDataViewModel: ObservableObject{
    
    @Published var timer = Timer()
    @Published var startTime : Double = 0.0
    @Published var secondsOriginal = 0.0
    @Published var secondsElapsed = 0.0
    @Published var secondsElapsed_ = 0.0
    @Published var minutesElapsed = 0.0
    @Published var hoursElapsed = 0.0
    
    enum stopWatchMode {
        case running
        case stopped
        case paused
    }
    
    init(){
//        start()
        print("initialized")
    }

    
    func start(){
            self.secondsOriginal = self.startTime
            self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true){ timer in
                self.secondsOriginal += 1
                self.secondsElapsed_ = Double(Int(self.secondsOriginal))
                self.secondsElapsed = Double(Int(self.secondsOriginal)%60)
                self.minutesElapsed = Double(Int(self.secondsOriginal)/60 % 60)
                self.hoursElapsed = Double(Int(self.secondsOriginal)/3600 % 24)
            }
    }
}

Solution

  • import SwiftUI
    import Combine
    struct SeizureView: View {
        
        
        @ObservedObject var apptCardVM: ApptCardViewModel
        //This will solve your issue.
        @StateObject var timerData = TimerDataViewModel()
        @State private var currentDate = Date()
        var body: some View {
            VStack {
                CurrentDateView(currentDate: $currentDate, timerData: timerData)  
                //But something to consider
                //This View is reusable and does not need a timer. Just a Date object
                TimerView(date: currentDate, showSubseconds: false)
                Picker("Seizure Type", selection: $apptCardVM.typeIndex) {
                    ForEach(0..<apptCardVM.typeChoice.count) {
                        Text(self.apptCardVM.typeChoice[$0])
                    }
                }.pickerStyle(SegmentedPickerStyle())
                
            }
            
        }
    }
    
    struct CurrentDateView: View {
        @Binding var currentDate : Date
        let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
        @ObservedObject var timerData: TimerDataViewModel
        var body: some View {
            
            Text("\(Int(timerData.hoursElapsed), specifier: "%02d"):\(Int(timerData.minutesElapsed), specifier: "%02d"):\(Int(timerData.secondsElapsed), specifier: "%02d")")
                .fontWeight(.bold)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .onAppear(){
                    timerData.start()
                }
        }
    }
    
    class ApptCardViewModel: ObservableObject, Identifiable {
        @Published var typeChoice = ["Quick", "Long", "FullService"]
        @Published var typeIndex: Int = 0
        private var cancellables = Set<AnyCancellable>()
        
        
    }
    
    class TimerDataViewModel: ObservableObject{
        
        @Published var timer = Timer()
        @Published var startTime : Double = 0.0
        @Published var secondsOriginal = 0.0
        @Published var secondsElapsed = 0.0
        @Published var secondsElapsed_ = 0.0
        @Published var minutesElapsed = 0.0
        @Published var hoursElapsed = 0.0
        
        enum stopWatchMode {
            case running
            case stopped
            case paused
        }
        
        init(){
            //        start()
            print("initialized")
        }
        
        
        func start(){
            self.secondsOriginal = self.startTime
            self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true){ timer in
                self.secondsOriginal += 1
                self.secondsElapsed_ = Double(Int(self.secondsOriginal))
                self.secondsElapsed = Double(Int(self.secondsOriginal)%60)
                self.minutesElapsed = Double(Int(self.secondsOriginal)/60 % 60)
                self.hoursElapsed = Double(Int(self.secondsOriginal)/3600 % 24)
            }
        }
    }
    
    struct TimerView: View {
        var date: Date
        var showSubseconds: Bool
        var fontWeight: Font.Weight = .bold
        
        var body: some View {
            if #available(watchOSApplicationExtension 8.0, watchOS 8.0, iOS 15.0, *) {
                //The code from here is mostly from https://developer.apple.com/wwdc21/10009
                TimelineView(MetricsTimelineSchedule(from: date)) { context in
                    ElapsedTimeView(elapsedTime: -date.timeIntervalSinceNow, showSubseconds: showSubseconds)
                }
            } else {
                Text(date,style: .timer)
                    .fontWeight(fontWeight)
                    .clipped()
            }
        }
    }
    @available(watchOSApplicationExtension 8.0, watchOS 8.0, iOS 15.0,*)
    private struct MetricsTimelineSchedule: TimelineSchedule {
        var startDate: Date
        
        init(from startDate: Date) {
            self.startDate = startDate
            
        }
        
        func entries(from startDate: Date, mode: TimelineScheduleMode) -> PeriodicTimelineSchedule.Entries {
            PeriodicTimelineSchedule(from: self.startDate, by: (mode == .lowFrequency ? 1.0 : 1.0 / 30.0))
                .entries(from: startDate, mode: mode)
        }
    }
    struct ElapsedTimeView: View {
        var elapsedTime: TimeInterval = 0
        var showSubseconds: Bool = false
        var fontWeight: Font.Weight = .bold
        @State private var timeFormatter = ElapsedTimeFormatter(showSubseconds: false)
        
        var body: some View {
            Text(NSNumber(value: elapsedTime), formatter: timeFormatter)
                .fontWeight(fontWeight)
                .onChange(of: showSubseconds) {
                    timeFormatter.showSubseconds = $0
                }
                .onAppear(perform: {
                    timeFormatter = ElapsedTimeFormatter(showSubseconds: showSubseconds)
                })
        }
    }
    
    class ElapsedTimeFormatter: Formatter {
        let componentsFormatter: DateComponentsFormatter = {
            let formatter = DateComponentsFormatter()
            formatter.allowedUnits = [.minute, .second, .hour]
            formatter.zeroFormattingBehavior = .pad
            return formatter
        }()
        var showSubseconds: Bool
        init(showSubseconds: Bool) {
            self.showSubseconds = showSubseconds
            super.init()
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        override func string(for value: Any?) -> String? {
            guard let time = value as? TimeInterval else {
                return nil
            }
            
            guard let formattedString = componentsFormatter.string(from: time) else {
                return nil
            }
            
            if showSubseconds {
                let hundredths = Int((time.truncatingRemainder(dividingBy: 1)) * 100)
                let decimalSeparator = Locale.current.decimalSeparator ?? "."
                return String(format: "%@%@%0.2d", formattedString, decimalSeparator, hundredths)
            }
            
            return formattedString
        }
    }
    struct SeizureView_Previews: PreviewProvider {
        static var previews: some View {
            SeizureView(apptCardVM: ApptCardViewModel())
        }
    }