Search code examples
swiftxcodeswiftuitimer

SwiftUI Timer is not starting


I have tried a couple different approaches to running a timer (onRecieve and scheduled) across three different applications within Xcode SwiftUI and have yet to see the timer work in the simulator. I followed the instructions from the following video for demonstration purposes:

https://www.youtube.com/watch?v=y68YmyTB7JU

I'm not sure if Xcode has a setting that needs to be modified to enable the timer in the simulator on my laptop or if I am missing something needed for a more recent version of Xcode/SwiftUI. This video is only about 6 months old as of this post though.

Thank you in advance!

Content View:

import SwiftUI

struct ContentView: View {
    @ObservedObject var stopWatchManager = StopWatchManager()
    
    var body: some View {
        VStack {
            Text(String(format: "%.1f", stopWatchManager.secondsElapsed))
                .font(.system(size: 50))
                .foregroundColor(.yellow)
                .padding(.top, 200)
                .padding(.bottom, 100)
                .padding(.trailing, 100)
                .padding(.leading, 100)
            
            if stopWatchManager.mode == .stopped{
                Button(action: {self.stopWatchManager.start()}){
                    TimerButton(label: "Start", buttonColor: .green, textColor: .black)
                }
            }
            
            if stopWatchManager.mode == .running{
                Button(action: {self.stopWatchManager.pause()}){
                    TimerButton(label: "Pause", buttonColor: .green, textColor: .black)
                }
            }
            
            if stopWatchManager.mode == .paused{
                Button(action: {self.stopWatchManager.start()}){
                    TimerButton(label: "Start", buttonColor: .green, textColor: .black)
                }
                Button(action: {self.stopWatchManager.stop()}){
                    TimerButton(label: "Stop", buttonColor: .red, textColor: .black)
                }
                .padding(.top, 30)
            }
            
            Spacer()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct TimerButton: View {
    
    let label: String
    let buttonColor: Color
    let textColor: Color
    
    var body: some View {
        Text(label)
            .foregroundColor(textColor)
            .padding(.vertical, 20)
            .padding(.horizontal, 90)
            .background(buttonColor)
            .cornerRadius(10)
    }
}

StopWatchManager:

import Foundation
import SwiftUI

class StopWatchManager: ObservableObject{
    
    enum stopWatchMode {
        case running
        case stopped
        case paused
    }
    
    @Published var mode: stopWatchMode = .stopped
    
    @Published var secondsElapsed = 0
    var timer = Timer()
    
    func start() {
        mode = .running
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
            timer in self.secondsElapsed += 1
        }
    }

    func pause() {
        timer.invalidate()
        mode = .paused
    }
        
    func stop() {
        timer.invalidate()
        secondsElapsed = 0
        mode = .stopped
    }
}

Solution

  • This is a sneaky bug that's hard to recognize.

    In your code, you've defined elapsedSeconds as:

    @Published var secondsElapsed = 0
    

    Swift does type inference to infer that secondsElapsed is an Int here. When you use String(format: "%.1f"...) it expects a Double (floating point number) and doesn't update properly with an Int.

    In the video code, you'll see that secondsElapsed is defined as:

    @Published var secondsElapsed = 0.0
    

    Swift infers this to be a Double -- if you make that change, everything will work as expected.

    PS: You can look into using Timer with Combine (eg Timer.publish) for a more SwiftUI-centric way of doing things. See the documentation