Home > Back-end >  SwiftUI Timer keep resetting when updating the ObservedObjects
SwiftUI Timer keep resetting when updating the ObservedObjects

Time:09-30

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: "d"):\(Int(timerData.minutesElapsed), specifier: "d"):\(Int(timerData.secondsElapsed), specifier: "d")")
            .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)
            }
    }
}

CodePudding user response:

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: "d"):\(Int(timerData.minutesElapsed), specifier: "d"):\(Int(timerData.secondsElapsed), specifier: "d")")
            .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())
    }
}
  • Related