How to cancel timer in SwiftUI view?

DevTechie Inc
Jan 15, 2023

Swift Timer class is used to schedule work to happen in future. With Timer we have option to execute code once or repeatedly.

If we want to run some SwiftUI code regularly, we can use Timer with onReceive modifier. Timer creates a publisher which fires at defined time and we can use that to execute our code. When we are done with the work we need to do, we don’t want to leave Timer running, instead we want to make sure its cancelled properly.

In this article, we will cover, how to cancel Timer in SwiftUI.

Let’s start with an example, we will create a view which has a colors array and a Rectangle shape for the view. We will use Timer to cycle through the colors by picking random color and change shape’s fill color.

struct DevTechieTimerResetExample: View {
    
    let colors = [Color.orange, .pink, .red, .green, .indigo, .cyan, .mint]
    
    @State private var fillColor = Color.orange
    
    var body: some View {
        VStack {
            RoundedRectangle(cornerRadius: 20)
                .fill(fillColor)
                .frame(width: 300, height: 200)
                .overlay{
                    Text("DevTechie")
                        .font(.largeTitle)
                        .foregroundColor(.white)
                }
        }
    }
}
Let’s create timer instance right after State variable with timer to fire every second.

let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
Next, we will attach onReceive modifier to the VStack and will update fillColor State variable with a random element picked from colors array.

.onReceive(timer) { _ in
    withAnimation {
        fillColor = colors.randomElement()!
    }
}
Build and run

Let’s stay we want to stop our timer with a Button. We will add a button and action for the button will connect to the upstream of timer publisher and cancel the timer.

Button {
    timer.upstream.connect().cancel()
} label: {
    Label("Stop", systemImage: "nosign")
}
Complete code:

struct DevTechieTimerResetExample: View {
    
    let colors = [Color.orange, .pink, .red, .green, .indigo, .cyan, .mint]
    
    @State private var fillColor = Color.orange
    
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    
    var body: some View {
        VStack {
            RoundedRectangle(cornerRadius: 20)
                .fill(fillColor)
                .frame(width: 300, height: 200)
                .overlay{
                    Text("DevTechie")
                        .font(.largeTitle)
                        .foregroundColor(.white)
                }
            HStack {
                Button {
                    timer.upstream.connect().cancel()
                } label: {
                    Label("Stop", systemImage: "nosign")
                }
                .buttonStyle(.bordered)
            }}
        .onReceive(timer) { _ in
            withAnimation {
                fillColor = colors.randomElement()!
            }
        }
    }
}
If we want to resume the timer, we will have to make some adjustments. Resuming timer requires reinitialization of the Timer so we will change our timer to a State property.

@State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
Reinitialize Timer with a button click

Button {
    timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
} label: {
    Label("Resume", systemImage: "play")
}
.buttonStyle(.borderedProminent)
Complete code:

struct DevTechieTimerResetExample: View {
    
    let colors = [Color.orange, .pink, .red, .green, .indigo, .cyan, .mint]
    
    @State private var fillColor = Color.orange
    
    @State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    
    var body: some View {
        VStack {
            RoundedRectangle(cornerRadius: 20)
                .fill(fillColor)
                .frame(width: 300, height: 200)
                .overlay{
                    Text("DevTechie")
                        .font(.largeTitle)
                        .foregroundColor(.white)
                }
            HStack {
                Button {
                    timer.upstream.connect().cancel()
                } label: {
                    Label("Stop", systemImage: "nosign")
                }
                .buttonStyle(.bordered)
                
                Button {
                    timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
                } label: {
                    Label("Resume", systemImage: "play")
                }
                .buttonStyle(.borderedProminent)
            }}
        .onReceive(timer) { _ in
            withAnimation {
                fillColor = colors.randomElement()!
            }
        }
    }
}
With that we have reached the end of this article. Thank you once again for reading. Don’t forget to follow 😍. Also subscribe our newsletter at https://www.devtechie.com