27

I've been watching the Data Flow Through SwiftUI WWDC talk. They have a slide with a sample code where they use a Timer publisher that gets connected to a SwiftUI View, and updates the UI with the time.

I'm working on some code where I want to do the exact same thing, but can't figure out how this PodcastPlayer.currentTimePublisher is implemented, and then hooked to the UI struct. I have also watched all the videos about Combine.

How can I achieve this?

The sample code:

struct PlayerView : View {
  let episode: Episode
  @State private var isPlaying: Bool = true
  @State private var currentTime: TimeInterval = 0.0

  var body: some View {
    VStack { // ...
      Text("\(playhead, formatter: currentTimeFormatter)")
    }
    .onReceive(PodcastPlayer.currentTimePublisher) { newCurrentTime in
      self.currentTime = newCurrentTime
    }
  }
}
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
eivindml
  • 2,197
  • 7
  • 36
  • 68

5 Answers5

48

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
    }
  }
}
kontiki
  • 37,663
  • 13
  • 111
  • 125
  • Awesome, thank you. So I guess in the example they have the `currentTimePublisher` as a static class variable. – eivindml Jul 25 '19 at 13:01
  • It looks like it. I haven't look at that exercise in detail. My posted answer is just one way to create a timer with Combine. Maybe there are others... – kontiki Jul 25 '19 at 13:03
  • Yes. And it works great. Just makes sense to have it as a static variable for them and my case, since it should ever only exist one timer. – eivindml Jul 25 '19 at 13:08
  • 2
    I think deinit don't need, as cancel() is called automatically when you deinit – zdravko zdravkin Nov 27 '20 at 15:26
  • if you use `let cancellable: Cancellable?` you don't need the `as? AnyCancellable` part – Daniel Aug 30 '23 at 14:22
17

Using ObservableObject

to Create a Timer Publisher using Swift Combine

class TimeCounter: ObservableObject {
    @Published var tick = 0
    
    lazy var timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in self.tick += 1 }
    init() { timer.fire() }
}

That's it! now you just need to observe for changes:

struct ContentView: View {
    @StateObject var timeCounter = TimeCounter()
    
    var body: some View {
        Text("\(timeCounter.tick)")
    }
}
Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
7

I implemented a Combine timer with a new feature allowing you to switch between different intervals.

class CombineTimer {

    private let intervalSubject: CurrentValueSubject<TimeInterval, Never>

    var interval: TimeInterval {
        get {
            intervalSubject.value
        }
        set {
            intervalSubject.send(newValue)
        }
    }

    var publisher: AnyPublisher<Date, Never> {
        intervalSubject
            .map {
                Timer.TimerPublisher(interval: $0, runLoop: .main, mode: .default).autoconnect()
            }
            .switchToLatest()
            .eraseToAnyPublisher()
    }

    init(interval: TimeInterval = 1.0) {
        intervalSubject = CurrentValueSubject<TimeInterval, Never>(interval)
    }

}

To start the timer, simply subscribe to the publisher property.

SomeView()
    .onReceive(combineTimer.publisher) { date in
        // ...
    }

You can switch to a new timer with a different interval by changing the interval property.

combineTimer.interval = someNewInterval
cayZ
  • 111
  • 1
  • 4
0

a timer that runs from 0 to 9.

struct PlayerView : View {

    @State private var currentTime: TimeInterval = 0.0  
    @ObservedObject var player = PodcastPlayer()        
    var body: some View {      
        Text("\(Int(currentTime))")
            .font(.largeTitle)
            .onReceive(player.$currentTimePublisher.filter { $0 < 10.0 }) { newCurrentTime in
                self.currentTime = newCurrentTime
        }
    }
}
class PodcastPlayer: ObservableObject {

    @Published var currentTimePublisher: TimeInterval = 0.0     
    init() {
        Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            self.currentTimePublisher += 1
        }
    }
}
David Buck
  • 3,752
  • 35
  • 31
  • 35
sajan
  • 1
0

just my 2 cents in a more general code from excellent sample from "kontiki"

    struct ContentView : View {
    @State private var currentTime: TimeInterval = 0.0
    @ObservedObject var counter = Counter(Δt: 1)
    var body: some View {
        VStack{
        Text("\(Int(currentTime))")
            .font(.largeTitle)
            .onReceive(counter.$currentTimePublisher) { newCurrentTime in
                self.currentTime = newCurrentTime
            }
        }
        Spacer().frame(height: 30)
        Button (action: { counter.reset() } ){
            Text("reset")
        }
        Spacer().frame(height: 30)
        Button (action: { counter.kill() } ){
            Text("KILL")
        }
        Spacer().frame(height: 30)
        Button (action: { counter.restart(Δt: 0.1) } ){
            Text("Restart")
        }

    }
}


class Counter: ObservableObject {

    @Published var currentTimePublisher: TimeInterval = 0.0
    
    private var timer: Timer?
    
    init(Δt: Double) {
        self.timer = Timer.scheduledTimer(withTimeInterval: Δt, repeats: true) { _ in
            self.currentTimePublisher += 1
        }
    }
    
    func restart(Δt: Double){
        kill()
        self.timer = Timer.scheduledTimer(withTimeInterval: Δt, repeats: true) { _ in
            self.currentTimePublisher += 1
        }
    }
    
    func kill(){
        self.timer?.invalidate()
        self.timer = nil
    }
    
    func reset(){
        currentTimePublisher = 0
    }
}
ingconti
  • 10,876
  • 3
  • 61
  • 48