👔TimerView
// 2022.03.24: refactored to be more flexible
// ------------------------------------------------------
// ⭐️ 潛藏問題:
//    如果同時使用兩個 TimerView,timer 竟然不是各自獨立❓
//    ( 👉 參看:「👁️ 預覽」)
// ------------------------------------------------------
import SwiftUI
import Combine
/// 👔 TimerView
/// ```
/// TimerView(every: 1) { ... }
/// TimerView(every: 1, update: { time in ... }) { ... }
/// ```
struct TimerView<Content: View>: View {
    
    // ⭐️ time interval on which to publish events
    var interval: TimeInterval = 1
    
    // ⭐️ update with time
    var update: (Date) -> Void = { _ in }
    
    // ⭐️ generate timer content
    @ViewBuilder var content: () -> Content 
    
    // ⭐️ timer that fires on the main thread.
    let timer: Publishers.Autoconnect<Timer.TimerPublisher>
        
    // init
    init(
        every interval: TimeInterval, 
        update: @escaping (Date) -> Void = {_ in},     // default: do nothing
        @ViewBuilder content: @escaping () -> Content
    ) {
        self.content = content
        self.update = update
        self.timer = Timer
            .publish(every: interval, on: .main, in: .common)
            .autoconnect()    // ⭐️ auto-connect when subscribed
    }
    
    // ⭐️ detect whether app has gone background
    @Environment(\.scenePhase) private var scenePhase
    @State private var isActive = true
    
    var body: some View {
        content()
            // ⭐️ whenever `timer` fires, update with time
            .onReceive(timer) { time in
                // ⭐️ if app goes background, stop updating immediately.
                guard isActive else { return }
                // ⭐️ update with time
                update(time)
            }
            // ⭐️ mark the app inactive once it goes background.
            .onChange(of: scenePhase) { newPhase in
                isActive = (newPhase == .active)
            }
    }
}struct ContentView: View {
    @State private var progress: CGFloat = 0
    @State private var timeLeft = 5
    let sec: TimeInterval = 4
    var body: some View {
        HStack {
            
            // TimerView 1
            TimerView(
                every: 1, 
                update: {_ in 
                    if timeLeft > 0 { timeLeft -= 1 }
                }
            ) { 
                Text("\(timeLeft)")
                    .font(.system(.title3, design: .rounded))
                    .frame(width: 80, height:40)
                    .background(Capsule().fill(.pink))
            }
            .padding(.trailing, 20)
            
            // TimerView 2
            TimerView(
                every: sec/100, 
                update: { _ in 
                    if progress < 1 { progress += 0.01 }
                }
            ) { 
                ZStack {
                    Circle()
                        .stroke(Color(.systemGray5), lineWidth: 14)
                    Circle()
                        .trim(from: 0, to: progress)
                        .stroke(.green, lineWidth: 12)
                        .rotationEffect(.degrees(-90))
                    Text("\(Int(progress * 100))%")
                        .font(.system(.title3, design: .rounded))
                }
            }
            .frame(100)
        }
        .padding()
    }
}- Foundation ⟩ Task Management ⟩ Timer (class) - Timer.TimerPublisher (class) - .autoconnect() -> Publishers.Autoconnect - <Timer.TimerPublisher>
 
 
- Combine ⟩ ConnectablePublisher (protocol) ⟩ - .connect() ⭐️ - connects to the publisher, allowing it to produce elements, and returns an instance with which to cancel publishing. 
 
- do animations with Timer. 
- example for how to implement default type parameter. 
- compare: TimerView (scheduledTimer) 
問:「 👁️ 預覽中的兩個 timer 竟然不是各自獨立,而是一前一後❓」
註:在類似的 TimerView (scheduledTimer) 中並沒有這種現象。
History
- 2022.03.24 
struct ContentView: View {
    var body: some View {
        HStack {
            
            // ⭐️ timer 1 (default timer content)
            TimerView(seconds: 10)
                .font(.system(.body, design: .monospaced))
            
            // ⭐️ timer 2 (custom timer content)
            TimerView(seconds: 15) { sec in
                Text("\(sec)")
                    .font(.title2)
                    .foregroundColor(.white)
                    .padding(.horizontal, 30)
                    .padding(.vertical, 5)
                    .background(.pink.opacity(0.75))
                    .clipShape(Capsule())
            }
            
            // ⭐️ timer 3 (custom timer content)
            TimerView(seconds: 20) { sec in
                ZStack {
                    Color.blue
                    Text("\(sec)")
                        .fixedSize()
                }
                .frame(50)
                .border(.secondary)
            }
        }
        .padding(8)
    }
}👔 TimerView
import SwiftUI
/// 👔 TimerView
/// ```
/// TimerView(seconds: 10)
/// TimerView(seconds: 10) { sec in ... }
/// ```
struct TimerView<Content: View>: View {
    
    // seconds remaining
    @State private var timeRemaining: Int
    
    // ⭐️ timer that fires once a second on the main thread.
    let timer = Timer
        .publish(every: 1, on: .main, in: .common)
        .autoconnect()    // ⭐️ auto-connect when subscribed
    
    // ⭐️ given seconds remaining, generate timer content
    @ViewBuilder var content: (Int) -> Content 
    
    // ⭐️ detect whether app has gone background
    @Environment(\.scenePhase) private var scenePhase
    @State private var isActive = true
    
    var body: some View {
        content(timeRemaining)
            // ⭐️ whenever `timer` fires, subtract 1 from `timeRemaining`
            .onReceive(timer) { time in
                // ⭐️ if app goes background, stop count-down immediately.
                guard isActive else { return }
                if timeRemaining > 0 { timeRemaining -= 1 }
            }
            // ⭐️ mark the app inactive once it goes background.
            .onChange(of: scenePhase) { newPhase in
                isActive = (newPhase == .active)
            }
    }
}
// ⭐️ timer with custom content
extension TimerView {
    
    /// `TimerView(seconds: 10) { sec in ... }`
    init(
        seconds: Int, 
        @ViewBuilder content: @escaping (Int) -> Content
    ){
        // ⭐️ initialize an @State property
        _timeRemaining = State(initialValue: seconds)
        self.content = content
    }
}
// ⭐️ timer with default content
extension TimerView where Content == Text
{
    /// `TimerView(seconds: 10)`
    init(seconds: Int){
        self.init(seconds: seconds) { sec in
            Text("\(sec)") as! Content
        }
    }
}Last updated
Was this helpful?