# TimerView

[Combine](..) ⟩ [Timer]() ⟩ TimerView

{% tabs %}
{% tab title="👔 TimerView" %}
````swift
// 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)
            }
    }
}
````
{% endtab %}

{% tab title="👁️ 預覽" %}
{% embed url="https://youtu.be/IntpSNuiR0I" %}

```swift
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()
    }
}
```
{% endtab %}

{% tab title="📗 參考" %}
* [x] Paul ⟩&#x20;
  * [x] [Counting down with a Timer](https://www.hackingwithswift.com/books/ios-swiftui/counting-down-with-a-timer)
  * [x] [Triggering events repeatedly using a timer](https://www.hackingwithswift.com/books/ios-swiftui/triggering-events-repeatedly-using-a-timer)
* [x] [Combine](https://developer.apple.com/documentation/combine) ⟩ [Replacing Foundation Timers with Timer Publishers](https://developer.apple.com/documentation/combine/replacing-foundation-timers-with-timer-publishers) ⭐️
{% endtab %}

{% tab title="📘 手冊" %}
* [Foundation ](https://developer.apple.com/documentation/foundation)⟩ [Task Management](https://developer.apple.com/documentation/foundation/task_management) ⟩ [Timer](https://developer.apple.com/documentation/foundation/timer) (<mark style="color:red;">**class**</mark>)
  * [.publish(every:tolerance:on:in:options:)](https://developer.apple.com/documentation/foundation/timer/3329589-publish) -> <mark style="color:purple;">**Timer**</mark>.[<mark style="color:orange;">**TimerPublisher**</mark>](https://developer.apple.com/documentation/foundation/timer/timerpublisher)
  * <mark style="color:purple;">**Timer**</mark>.[<mark style="color:orange;">**TimerPublisher**</mark>](https://developer.apple.com/documentation/foundation/timer/timerpublisher) (<mark style="color:red;">**class**</mark>)
    * [.autoconnect()](https://developer.apple.com/documentation/foundation/timer/timerpublisher/3329490-autoconnect) -> [Publishers](https://developer.apple.com/documentation/combine/publishers).[<mark style="color:orange;">**Autoconnect**</mark>](https://developer.apple.com/documentation/combine/publishers/autoconnect)`<Timer.TimerPublisher>`&#x20;
* [Combine](https://developer.apple.com/documentation/combine) ⟩ [ConnectablePublisher](https://developer.apple.com/documentation/combine/connectablepublisher) (<mark style="color:orange;">**protocol**</mark>) ⟩&#x20;
  * [.connect()](https://developer.apple.com/documentation/combine/connectablepublisher/connect\(\)) ⭐️ - connects to the publisher, allowing it to <mark style="color:orange;">**produce elements**</mark>, and <mark style="color:red;">**returns an instance**</mark> with which to <mark style="color:orange;">**cancel**</mark> publishing.
{% endtab %}

{% tab title="⬇️ 應用" %}
* [update-gesture-states](../../../../../../swiftui/gestures/update-gesture-states "mention")
{% endtab %}

{% tab title="👥 相關" %}
* do [anim](../../../../../../swiftui/anim "mention") with [](<> "mention").
* example for how to implement [default-type-parameter](../../../../../type/category/generic-types/default-type-parameter "mention").
* [examples](../../../../../../swiftui/anim/examples "mention")
* compare: [timerview-scheduledtimer](timerview-scheduledtimer "mention")
{% endtab %}

{% tab title="❓" %}
{% hint style="warning" %}
問：「 👁️ 預覽中的兩個 timer 竟然不是各自獨立，而是一前一後❓」

註：在類似的 [timerview-scheduledtimer](timerview-scheduledtimer "mention") 中並沒有這種現象。
{% endhint %}
{% endtab %}
{% endtabs %}

## History

{% tabs %}
{% tab title="✏️" %}
1. 2022.03.24
{% endtab %}

{% tab title="1" %}
{% embed url="https://youtu.be/YO4n_HekzGs" %}

```swift
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

````swift
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
        }
    }
}
````
{% endtab %}
{% endtabs %}
