๐TimerView
Combine โฉ Timer โฉ 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