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?