TimerView
Last updated
Was this helpful?
Last updated
Was this helpful?
⟩ ⟩ 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()
}
}
do animations with Timer.
example for how to implement default type parameter.
compare: TimerView (scheduledTimer)
問:「 👁️ 預覽中的兩個 timer 竟然不是各自獨立,而是一前一後❓」
註:在類似的 TimerView (scheduledTimer) 中並沒有這種現象。
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
}
}
}
⟩ ⭐️
⟩ ⟩ (class)
-> Timer.
Timer. (class)
-> .<Timer.TimerPublisher>
⟩ (protocol) ⟩
⭐️ - connects to the publisher, allowing it to produce elements, and returns an instance with which to cancel publishing.