Timer running at increased speed each time I start it again

I am currently working on an app, and it has a timer in a separate view from the main ContentView(). I have realized that I am not resetting the timer each time, and since it is initialized with a binding var, the var is increasing faster than desired (add one to var every 0.01 seconds). Simply setting the var to a binding and changing it that way hasn't worked for some reason, and I have created a function that runs every time the timer fires to check if the isRunning binding var is false, and if it is, invalidate the timer. The code for ContentView is:

import Foundation
import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(entity: Time.entity(), sortDescriptors: [])
    private var times: FetchedResults<Time>
    
    @State var startstop: String = "Start"
    @State var start: Bool = false
    @State var elapsedTime: Int = 0
    @State var timeMinutes: Int = 0
    @State var timeSeconds: Int = 0
    @State var timeMilliseconds: Int = 0
    @State var hour: Int = 0
    @State var minute: Int = 0
    @State var second: Int = 0
    @State var formattedTime: String = ""
    @State var saveTime: Int = 0
    @State var timerRunning: Bool = true
    @State var firstRun: Bool = true
    // Format date after core data fetch to allow creating NSSortDescript for date
    var body: some View {
        VStack{
            Text("Cube App")
                .font(.largeTitle)
                .fontWeight(.bold)
                .padding(.top, 4.0)
            Text(scrambles.randomElement()!)
                .font(.title)
                .fontWeight(.semibold)
                .multilineTextAlignment(.center)
                .padding([.top, .leading, .trailing])
            if start{
                Stopwatch(progressTime: $elapsedTime, isRunning: $timerRunning)
                    .padding(.top)
                    .fontWeight(.bold)
                    .onAppear{
                        timerRunning = true
                        toggleStartStop()
                    }
            }
                
            Button(startstop){
                toggleStart()
                if start == false{
                    saveTime = elapsedTime
                    stopTime()
                    elapsedTime = 0
                    toggleStartStop()
                }
            }
            .fontWeight(.bold)
            .font(.system(size: 30))
            .padding(.top, 30)
            Spacer()
        }
    }
    
    private func stopTime(){
        timerRunning = false
        print(saveTime)
        addTime()
    }
    
    private func addTime(){
        withAnimation{
            timeMinutes = saveTime / 6000
            timeSeconds = (saveTime % 6000) / 100
            timeMilliseconds = saveTime % 100
            let time = Time(context: viewContext)
            if timeMinutes == 0{
                if timeMilliseconds < 10{
                    time.time = "\(timeSeconds).0\(timeMilliseconds)"
                }else{
                    time.time = "\(timeSeconds).\(timeMilliseconds)"
                }
            }else if timeMilliseconds < 10{
                if timeSeconds < 10{
                    time.time = "\(timeMinutes):0\(timeSeconds).0\(timeMilliseconds)"
                }else{
                    time.time = "\(timeMinutes):\(timeSeconds).0\(timeMilliseconds)"
                }
            }else{
                if timeSeconds < 10{
                    time.time = "\(timeMinutes):0\(timeSeconds).\(timeMilliseconds)"
                }else{
                    time.time = "\(timeMinutes):\(timeSeconds).\(timeMilliseconds)"
                }
            }
            time.date = Date()
            
            saveContext()
        }
    }
    
    private func saveContext(){
        do {
            try viewContext.save()
        } catch {
            let error = error as NSError
            print(error)
            fatalError(error as! String)
        }
    }
    
    private func toggleStart(){
        if start{
            start = false
        }else{
            start = true
        }
    }

    private func toggleStartStop(){
        if startstop == "Start"{
            startstop = "Stop"
        }else{
            startstop = "Start"
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        let persistenceController = PersistenceController.shared
        ContentView().environment(\.managedObjectContext, persistenceController.container.viewContext)
    }
}

Note: I have excluded the array of "scrambles" as it went over the character limit, but it is only used in a text in the vstack. The code for the Stopwatch (Timer) file is:

import SwiftUI

struct Stopwatch: View {
    @Binding var progressTime: Int
    @Binding var isRunning: Bool
    /// Computed properties to get the progressTime in hh:mm:ss format
    var hours: Int {
        progressTime / 6000
    }

    var minutes: Int {
        (progressTime % 6000) / 100
    }

    var seconds: Int {
        progressTime % 100
    }
    
    var timer: Timer {
        Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) {_ in
            progressTime += 1
            reset()
        }
    }
    
    var body: some View {
      HStack(spacing: 2) {
          StopwatchUnitView(timeUnit: hours)
          Text(":")
          StopwatchUnitView(timeUnit: minutes)
          Text(".")
          StopwatchUnitView(timeUnit: seconds)
      }.onAppear(perform: { _ = timer })
    }
    func reset(){
        if isRunning == false{
            self.timer.invalidate()
            print("invailidated")
        }
    }
    func toggleRun(){
        if isRunning{
            isRunning = false
            progressTime = 0
            print(isRunning)
            timer.invalidate()
        }else{
            isRunning = true
        }
    }
    func fetchTime() -> Int{
        return progressTime
    }
}


struct StopwatchUnitView: View {

    var timeUnit: Int

    /// Time unit expressed as String.
    /// - Includes "0" as prefix if this is less than 10
    var timeUnitStr: String {
        let timeUnitStr = String(timeUnit)
        return timeUnit < 10 ? "0" + timeUnitStr : timeUnitStr
    }

    var body: some View {
        HStack (spacing: 2) {
            Text(timeUnitStr.substring(index: 0)).frame(width: 10)
            Text(timeUnitStr.substring(index: 1)).frame(width: 10)
        }
    }
}

extension String {
    func substring(index: Int) -> String {
        let arrayString = Array(self)
        return String(arrayString[index])
    }
}

struct stopwatch_Previews: PreviewProvider {
    static var previews: some View {
        Stopwatch(progressTime:.constant(0), isRunning: .constant(true) /*firstRun: .constant(true)*/)
    }
}

Could somebody help me reset the timer each time and fix the issue?

I would restructure this so that the views observe a timer, which is an ObservableObject. From one of my (older) apps you may not want to directly follow:

class CountDownClock: ObservableObject {

   @Published var ticksLeft: Double = Constants.startTickCount
   @Published var alertNow: Bool = false

   private var targetDate: Date = Date(timeInterval: Double(Constants.startTickCount)/Double(Constants.ticsPerSecond), since: Date())
   
   private var alarmSound: AVAudioPlayer?

   private var timer: Timer?
   private var counterTick: Int = 0
...
   public func start(withReset reset: Bool) {
...

CountDownClock would have the start, stop, reset etc functions called from view controls. For example, below the ContentView has a tap gesture defined and when the user taps the subview, the timer is either started or reset.

The views then just update their state when the timer ticks since the clock is an ObservedObject:

struct ContentView: View {

   @ObservedObject var clock: CountDownClock

   var pauseTap : some Gesture {
      TapGesture(count: 1)
         .onEnded { _ in
            if self.clock.isTicking() {
               self.clock.stop()
            } else {
               self.clock.start(withReset: false)
            }
      }
   }
...
   var body: some View {
...
       VStack {
...
               Text(self.clock.description)
                  .gesture(self.pauseTap)
       }
      .gesture(self.resetTap)
...

By the way, Bool already has a .toggle() so you do not have to implement your own toggleStart().

Thanks for your fast reply. I tried implimenting what you said would work, but I wasn't able to figure out how to do it. Could you please provide a simpler way/make this more clear?

Sorry the delay, haven't stumbled on this discussion for a while. I am not quite sure if you need to restructure everything to make this work. But I have found out that having a model object (as in Model-View-Controller or Model-View-ViewModel architecture) is usually a good way to structure SwiftUI apps.

  • The view simply displays things from the Model object and provides the user a way to manipulate the data (buttons, gestures, etc.).
  • The model object contains the data and provides functions to manipulate it that the views call.
  • Model is an ObservableObject having @Published properties. When those properties change, views are recreated with the new data values.

This is what I was suggesting. Every time the timer fires in the Model object, it changes the value(s) of a/some @Published properties, causing the views to be updated.

I have a demo app visualising some sorting algorithms that uses a timer. Basically shows what I described above, but is unfortunately a bit complex for your needs. Anyways, take a look if you still need to.

The class SortCoordinator is here the Model, coordinating the visualisation of sorting algorithms, using a timer. As the timer fires, a step in sorting is done, changing positions of items in an array. The array is a @Published property, so the view updates when two numbers in the array change places while the sorting is advancing.

The ContentView just shows the data from the SortCoordinator, view being updated whenever the @Published properties change.

Search for "timer" in SortCoordinator to see how it is used there and how it updates the properties.

Timer running at increased speed each time I start it again
 
 
Q