Create a Timer Publisher using Swift Combine
Here you have an example of a Combine timer. I am using a global, but of course you should use whatever is applicable to your scenario (environmentObject, State, etc).
import SwiftUI
import Combine
class MyTimer {
let currentTimePublisher = Timer.TimerPublisher(interval: 1.0, runLoop: .main, mode: .default)
let cancellable: AnyCancellable?
init() {
self.cancellable = currentTimePublisher.connect() as? AnyCancellable
}
deinit {
self.cancellable?.cancel()
}
}
let timer = MyTimer()
struct Clock : View {
@State private var currentTime: Date = Date()
var body: some View {
VStack {
Text("\(currentTime)")
}
.onReceive(timer.currentTimePublisher) { newCurrentTime in
self.currentTime = newCurrentTime
}
}
}
Swift Combine: Using timer publisher in an observable object
This is a bit different to your original but nothing important is changed I hope.
import Combine
import SwiftUI
class TimerViewModel: ObservableObject {
private var assignCancellable: AnyCancellable? = nil
@Published var tick: String = "0:0:0"
init() {
assignCancellable = Timer.publish(every: 1.0, on: .main, in: .default)
.autoconnect()
.map { String(describing: $0) }
.assign(to: \TimerViewModel.tick, on: self)
}
}
struct ContentView: View {
@State private var currentTime: String = "Initial"
@ObservedObject var viewModel = TimerViewModel()
var body: some View {
VStack {
Text(currentTime)
Text(viewModel.tick) // why doesn't this work?
}
.onReceive(Timer.publish(every: 0.9, on: .main, in: .default).autoconnect(),
perform: {
self.currentTime = String(describing: $0)
}
)
}
}
I made viewModel an ObservedObject just to simplify the code.
The Timer.publish method along with autoconnect make Timer easier to use. I have found that using the same publisher with multiple subscribers causes problems as the first cancel kills the publisher.
I removed the deinit() as the cancel seems to be implicit for subscribers.
There was an interference between updates from onReceive and viewModel but changing the onReceive to 0.9 fixed that.
Finally I have discovered that the print() method in Combine is very useful for watching pipelines.
How do you run a Swift Combine Publisher for a certain amount of time?
This operator will do the trick.
import PlaygroundSupport
import Foundation
import Combine
let page = PlaygroundPage.current
page.needsIndefiniteExecution = true
extension Publisher {
func stopAfter<S>(_ interval: S.SchedulerTimeType.Stride, tolerance: S.SchedulerTimeType.Stride? = nil, scheduler: S, options: S.SchedulerOptions? = nil) -> AnyPublisher<Output, Failure> where S: Scheduler {
prefix(untilOutputFrom: Just(()).delay(for: interval, tolerance: tolerance, scheduler: scheduler, options: nil))
.eraseToAnyPublisher()
}
}
let source = Timer.publish(every: 1, tolerance: nil, on: RunLoop.main, in: .default, options: nil)
.autoconnect()
.eraseToAnyPublisher()
let cancellable = source
.stopAfter(10, scheduler: DispatchQueue.main)
.sink(receiveValue: { print($0) })
Combine Timer.TimerPublisher not starting
You have to call autoconnect()
on the Timer
publisher to fire it.
private init() {
NSLog("TokenObserver.init()")
self.cancellable = self.publisher.autoconnect().sink(receiveCompletion: { completion in
NSLog("TokenObserver \(completion)")
}, receiveValue: { date in
NSLog("TokenObserver timestamp=" + ISO8601DateFormatter().string(from: date))
})
}
Combine Timer - setting timer
You can significantly reduce complexity in your update
function. There's no need to create a new publisher with Just
to get the date. Plus, you can already get that in your initial sink
:
class Example : ObservableObject {
private var cancellable: AnyCancellable? = nil
@Published var somedate: Date = Date()
init() {
cancellable = Timer
.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink(receiveValue: { value in
self.update(date: value)
})
}
func update(date: Date) {
self.somedate = date
print(self.somedate)
}
}
Timer publisher with initial fire now
Alternatively to creating another publisher, you can simply prepend
to your timer publisher:
Timer
.publish(every: 10, on: .main, in: .common)
.autoconnect()
.prepend(Date())
.map { _ in ... }
.sink(receiveValue: { [weak self] in
...
})
.store(in: &subscriptions)
The above code has the same effect of publishing a value right away, and let the timer publish the other values.
Timer publisher init timer after button click
Just put the timer in a child view and control its visibility with a bool. When the TimerView
is removed the state is destroyed and the timer stops.
struct ContentView: View {
@State var started = false
var body: some View {
VStack {
Button(started ? "Stop" : "Start") {
started.toggle()
}
if started {
TimerView()
}
}
}
}
struct TimerView: View {
@State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
...
How to trigger Timer.publish() right away?
I'm sure there are better ways than mapping and replacing the error
Timer.TimerPublisher.Failure
is Never
, so you don't need any mapping because it can't fail.
Furthermore, Timer.TimerPublisher.Output
is already the current Date()
, so you don't need to map
the output either.
To emit the current date immediately on subscription, you want to combine a Deferred { Just(Date()) }
with the timer publisher. The Deferred
means the current date won't be computed until the subscription happens.
Here is one way to put it all together:
let timer = Deferred { Just(Date()) }
.append(Timer.publish(every: 5, on: .main, in: .common).autoconnect())
.eraseToAnyPublisher()
Related Topics
Swift: Uicollectionview Selecting Cell Indexpath Issues
Swiftui MACos Nswindow Instance
Adding Activity Indicator to Uialertview
Swift Tableview Cell Set Accessory Type
Resetting Zone Allocator with Allocations Still Alive
How to Fix ' *Pod* Does Not Support Provisioning Profiles' in Azure Devops Build Agent
Load a Pcm into a Avaudiopcmbuffer
Using a Mtltexture as the Environment Map of a Scnscene
How to Sort Objects by Its Enum Value
How to Recognize Continuous Touch in Swift
Error Using Associated Types and Generics
How to Zip More Than 4 Publishers
How to Set Title of Navigation Bar in Swift
How to Convert Copaquepointer in Swift to Some Type (Cgcontext? in Particular)
Convincing Swift That a Function Will Never Return, Due to a Thrown Exception