Search code examples
swiftswiftuitimercombinepublisher

SwiftUI - Update Model in the Background on a Schedule


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.


Solution

  • 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.