Search code examples
swiftswiftuitimer

SwiftUI- Timer auto adds 30 minutes to countdown


I'm trying to make a timer in swiftui but whenever I run it, it auto adds 30 minutes to the countdown. For example, when I set the countdown time to 5 minutes and click the "Start" button, it will show up as 35 minutes instead but when I click the button again, it will then just keep switching to random times. Above is the random times it will switch to.enter image description here

I got this timer from a tutorial on youtube by Indently but changed some things to fit what I wanted it to do. I tried to set a custom time so the timer will always countdown from 5 minutes. From my understanding, the timer works by taking the difference between the current date and the end date then using the amount of time difference as the countdown. Below is the code for the TimerStruct (ViewModel) and the TimerView.

TimerStruct:

import Foundation

extension TimerView {
    final class ViewModel: ObservableObject {
        @Published var isActive = false
        @Published var showingAlert = false
        @Published var time: String = "5:00"
        @Published var minutes: Float = 5.0 {
            didSet {
                self.time = "\(Int(minutes)):00"
            }
        }
         var initialTime = 0
         var endDate = Date()
        
        // Start the timer with the given amount of minutes
        func start(minutes: Float) {
            self.initialTime = 5
            self.endDate = Date()
            self.isActive = true
            self.endDate = Calendar.current.date(byAdding: .minute, value: Int(minutes), to: endDate)!
        }
        
        // Reset the timer
        func reset() {
            
            self.minutes = Float(initialTime)
            self.isActive = false
            self.time = "\(Int(minutes)):00"
        }
        
        // Show updates of the timer
        func updateCountdown(){
            guard isActive else { return }
            
            // Gets the current date and makes the time difference calculation
            let now = Date()
            let diff = endDate.timeIntervalSince1970 - now.timeIntervalSince1970
            
            // Checks that the countdown is not <= 0
            if diff <= 0 {
                self.isActive = false
                self.time = "0:00"
                self.showingAlert = true
                return
            }
            
            // Turns the time difference calculation into sensible data and formats it
            let date = Date(timeIntervalSince1970: diff)
            let calendar = Calendar.current
            let minutes = calendar.component(.minute, from: date)
            let seconds = calendar.component(.second, from: date)

            // Updates the time string with the formatted time
            self.minutes = Float(minutes)
            self.time = String(format:"%d:%02d", minutes, seconds)
        }
    }
}

TimerView:

import SwiftUI

struct TimerView: View {
    @ObservedObject var vm = ViewModel()
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    let width: Double = 250
    
    var body: some View {
        VStack {
            
            Text("Timer: \(vm.time)")
                .font(.system(size: 50, weight: .medium, design: .rounded))
                .alert("Timer done!", isPresented: $vm.showingAlert) {
                    Button("Continue", role: .cancel) {
                        
                    }
                }
                .padding()
            
            HStack(spacing:50) {
                Button("Start") {
                    vm.start(minutes: Float(vm.minutes))
                }
                .padding()
                .background((Color(red: 184/255, green: 243/255, blue: 255/255)))
                .foregroundColor(.black)
                .cornerRadius(10)
                .font(Font.system(size: UIFontMetrics.default.scaledValue(for: 16)))
                //.disabled(vm.isActive)
                
                if vm.isActive == true {
                    Button("Pause") {
                        vm.isActive = false
                        //self.timer.upstream.connect().cancel()
                    }
                    .padding()
                    .foregroundColor(.black)
                    .background(.red)
                    .cornerRadius(10)
                    .font(Font.system(size: UIFontMetrics.default.scaledValue(for: 16)))
                } else {
                    Button("Resume") {
                        vm.isActive = true
                    }
                    .padding()
                    .foregroundColor(.black)
                    .background(.green)
                    .cornerRadius(10)
                    .font(Font.system(size: UIFontMetrics.default.scaledValue(for: 16)))
                }
            }
            .frame(width: width)
            }
            .onReceive(timer) { _ in
                vm.updateCountdown()
            }
            
        }
    }


struct TimerView_Previews: PreviewProvider {
    static var previews: some View {
        TimerView()
    }
}

Solution

  • I noticed and fixed a number of things in your code:

    1. start() is being called with the current value of vm.minutes, so it is going to start from that value and not 5. I changed it to use self.initialTime which means it's currently not using the value passed in. You need to decide if start() really wants to take a value and how to use it.

    2. reset() wasn't being called. I call it from start().

    3. Pause was only pausing the screen update. I changed it to keep track of the start time of the pause and to compute the amount of time paused so that it could accurately update the displayed time.

    4. I made the Pause/Resume button one button with conditional values for title and color based upon vm.active.


    Here is the updated code:

    extension TimerView {
        final class ViewModel: ObservableObject {
            @Published var isActive = false
            @Published var showingAlert = false
            @Published var time: String = "5:00"
            @Published var minutes: Float = 5.0 {
                didSet {
                    self.time = "\(Int(minutes)):00"
                }
            }
            var initialTime = 0
            var endDate = Date()
            var pauseDate = Date()
            var pauseInterval = 0.0
            
            // Start the timer with the given amount of minutes
            func start(minutes: Float) {
                self.initialTime = 5
                self.reset()
                self.endDate = Date()
                self.endDate = Calendar.current.date(byAdding: .minute, value: self.initialTime, to: endDate)!
                self.isActive = true
            }
            
            // Reset the timer
            func reset() {
                self.isActive = false
                self.pauseInterval = 0.0
                self.minutes = Float(initialTime)
                self.time = "\(Int(minutes)):00"
            }
            
            func pause() {
                if self.isActive {
                    pauseDate = Date()
                } else {
                    // keep track of the total time we're paused
                    pauseInterval += Date().timeIntervalSince(pauseDate)
                }
                self.isActive.toggle()
            }
            
            // Show updates of the timer
            func updateCountdown(){
                guard isActive else { return }
                
                // Gets the current date and makes the time difference calculation
                let now = Date()
                let diff = endDate.timeIntervalSince1970 + self.pauseInterval - now.timeIntervalSince1970
                
                // Checks that the countdown is not <= 0
                if diff <= 0 {
                    self.isActive = false
                    self.time = "0:00"
                    self.showingAlert = true
                    return
                }
                
                // Turns the time difference calculation into sensible data and formats it
                let date = Date(timeIntervalSince1970: diff)
                let calendar = Calendar.current
                let minutes = calendar.component(.minute, from: date)
                let seconds = calendar.component(.second, from: date)
                
                // Updates the time string with the formatted time
                //self.minutes = Float(minutes)
                self.time = String(format:"%d:%02d", minutes, seconds)
            }
        }
    }
    
    struct TimerView: View {
        @ObservedObject var vm = ViewModel()
        let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
        let width: Double = 250
        
        var body: some View {
            VStack {
                
                Text("Timer: \(vm.time)")
                    .font(.system(size: 50, weight: .medium, design: .rounded))
                    .alert("Timer done!", isPresented: $vm.showingAlert) {
                        Button("Continue", role: .cancel) {
                            
                        }
                    }
                    .padding()
                
                HStack(spacing:50) {
                    Button("Start") {
                        vm.start(minutes: Float(vm.minutes))
                    }
                    .padding()
                    .background((Color(red: 184/255, green: 243/255, blue: 255/255)))
                    .foregroundColor(.black)
                    .cornerRadius(10)
                    .font(Font.system(size: UIFontMetrics.default.scaledValue(for: 16)))
                    //.disabled(vm.isActive)
                    
                    Button(vm.isActive ? "Pause" : "Resume") {
                        vm.pause()
                        //vm.isActive = false
                        //self.timer.upstream.connect().cancel()
                    }
                    .padding()
                    .foregroundColor(.black)
                    .background(vm.isActive ? .red : .green)
                    .cornerRadius(10)
                    .font(Font.system(size: UIFontMetrics.default.scaledValue(for: 16)))
                    
                }
                .frame(width: width)
            }
            .onReceive(timer) { _ in
                vm.updateCountdown()
            }
            
        }
    }