โœจcancellable timer

๐Ÿ“— ๅƒ่€ƒ๏ผšPaul โŸฉ Counting down with a Timer

๐Ÿ‘ฅ ็›ธ้—œ๏ผš.onChange(), .onRecieve() Events.

import SwiftUI

struct TimerView: View {
    
    // gives the user 100 seconds to start with
    @State private var timeRemaining = 100
    @State private var isTimerStopped = false
    @State private var isTimerCancelled = false
    
    // โญ๏ธ timer that fires once a second on the main thread.
    let timer = Timer
        // โญ๏ธ Timer.TimerPublisher
        .publish(
            every: 1,       // timer fires every 1 second.
            on: .main,      // run on the .main thread (UI thread).
            in: .common     // run on the .common run loop.
            // Run loop lets iOS handle running code while the user is actively
            // doing something, such as scrolling in a list
        )
        // โญ๏ธ connect automatically when a subscriber attaches
        .autoconnect()
    
    // โญ๏ธ detect whether app has gone background
    @Environment(\.scenePhase) private var scenePhase
    @State private var isActive = true
    
    var body: some View {
        VStack {
            timerView
            HStack {
                stopButton
                cancelButton
            }
        }
        .padding(8)
        .border(.secondary)
    }
    
    /// text for timer
    var text: some View {
        Text("Time: \(timeRemaining)")
            .font(.largeTitle)
            .foregroundColor(isTimerCancelled ? .secondary : .white)
            .padding(.horizontal, 20)
            .padding(.vertical, 5)
            .background(.pink.opacity(0.75))
            .clipShape(Capsule())
    }
    
    /// timer view
    var timerView: some View {
        text
            // โญ๏ธ connect `timer` automaticallyโ“
            .onReceive(timer) { time in
                // โญ๏ธ if app goes background or timer stopped, stop count-down immediately.
                guard isActive && !isTimerStopped else { return }
                // โญ๏ธ count down
                if timeRemaining > 0 { timeRemaining -= 1 }
            }
            // โญ๏ธ mark the app inactive once it goes background.
            .onChange(of: scenePhase) { newPhase in
                isActive = (newPhase == .active)
            }
    }
    
    /// Stop/Resume button
    var stopButton: some View {
        Button {
            isTimerStopped.toggle()
        } label: {
            Text(isTimerStopped ? "Resume" : "Stop")
        }
        .disabled(isTimerCancelled)
    }
    
    /// cancel button
    var cancelButton: some View {
        Button {
            // โญ๏ธ cancel the timer
            timer               // Publishers.Autoconnect<Timer.TimerPublisher>
                .upstream       // Timer.TimerPublisher (ConnectablePublisher)
                .connect()      // allow to produce elements, return an instance (Cancellable)
                .cancel()       // โญ๏ธ
            isTimerCancelled = true
        } label: {
            Text("Cancel")
        }
        .disabled(isTimerCancelled)
    }
}

Last updated