0

I have a simulation model that should update itself variably once every 0.5 - 5 seconds (roughly) in the background with the frequency a function of the model's state.

I am using a SwiftUI app that displays some of the data from this simulation model, and the SwiftUI interface already pulls the published data from the model just fine. I currently have some external controls (e.g. a slider, a toggle button, etc.) that I can use to manipulate the state of the model and this is reflected in the SwiftUI views that display the data. What I want to do now is make the model update itself on a timer (again, it's a simulator) and when the published data changes, SwiftUI already handles having the views re-draw themselves with the updated data.

So what I'd like to do:

class MyModel: ObservableObject {
   static let shared shared = MyModel()

   @Published var coolData1: CoolData()
   @Published var coolData2: CoolData()
   @Published var coolData3: CoolData()

   var modelTimer =  AnyPublisher<Date,Never>

   func runModel() {
       modelTimer = Timer.publish(every: 1, on: .main, in: .common)
                     .autoconnect()
                     .eraseToAnyPublisher()  
        // HERE I want updateModel() called every time the modelTimer fires and it should run in the background. 
   }

   func pauseModel() {
       modelTimer.upstream.connect().cancel()
   }

   func updateModel() {
       update(coolData1)
       update(coolData2)
       update(coolData3)
   } 
}

I just don't know where or the specific syntax with Combine/Timer to get the model to update on my dynamic schedule so it runs in the background.

Thank you for your help.

  • why on a timer and not on event? You know when changes were made (if I understood correctly), so why not use that knowledge? E.g. via property observers, or listener, or something of that nature. And if you are worried that updates will be too frequent, the "cooldown period" pattern can help with that. But that way you only call update as many times as needed, and no expensive timer to upkeep. – timbre timbre Oct 11 '22 at 22:59
  • Because the events are internally triggered in the model. There are additionally external events that are triggered that impact the model so in actuality, updateModel() could be called both automatically on a schedule plus when some user-driven event occurs. – Productions Oct 11 '22 at 23:03

1 Answers1

0

You already created the timer publisher, but you don't have anyone subscribed to it.

To subscribe to it you can use the sink publisher operator. I'm typing this code directly into the response so you may need to tweak it. First, add a field to your class to hold the subscription:

class MyModel: ObservableObject {
   static let shared shared = MyModel()
   var timerSubscription: AnyCancellable?

Then after you set up your timer, subscribe to it with sink

    // HERE I want updateModel() called every time the modelTimer fires and it should run in the background. 
    timerSubscription = modelTimer.sink { [weak self] _ in
       guard nil != self { return }
       self!.updateModel()
    }

Now something I note, your timer is set up to fire on the main run loop, so at the moment the subscription will not be run in the background.

You have to capture the subscription. If you do not then the call to sink will subscribe then immediately cancel the subscription. If at any point you want to cancel the subscription you can use timerSubscription.cancel.

Scott Thompson
  • 22,629
  • 4
  • 32
  • 34