Create a Timer Publisher Using Swift Combine

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



Leave a reply



Submit