Home > Mobile >  How to check for staleness of data in Combine with Timers
How to check for staleness of data in Combine with Timers

Time:12-09

I am converting some code into Combine in order to get familiar with it. I am doing fine with the easy stuff, but here it gets a little trickier. I am trying to report to the user when incoming GPS data is accurate and also whether it's stale.

So I have

let locationPublisher = PassthroughSubject<CLLocation,Never>()
private var cancellableSet: Set<AnyCancellable> = []
var status:GPSStatus = .red //enum 

and in init I have

locationPublisher
        .map(gpsStatus(from:)) //maps CLLocation to GPSStatus enum
        .assign(to: \.gpsStatus, on: self)
        .store(in: &cancellableSet)

locationPublisher.sink() { [weak self] location in
        self?.statusTimer?.invalidate()
        self?.setStatusTimer()
}
    .store(in: &cancellableSet)

setStatusTimer()

Here is the setStatusTimer function

func setStatusTimer () {
    statusTimer = Timer.scheduledTimer(withTimeInterval: 20, repeats: false)   {@MainActor _ in
          self.updateGPSStatus(.red)
    }
}

Is there a more "Combine" way of doing this? I know there are Timer.TimerPublishers, but I'm not sure how to incorporate them?

My tendency is to think there is some kind of combineLatest with one input being the gps status publisher and the other one being some kind go publisher that fires if the upstream pub hasn't fired for x seconds.

Thanks!

CodePudding user response:

This is a bit tricky. You don't need a TimerPublisher but you can use the timeout operator. The tricky part is that timeout will create a publisher that stops publishing when it times out. The question is "how do you start again". To do that, you can use the catch operator.

The solution looks like this:

import UIKit
import Combine

enum GPSStatus {
    // Karma Chameleon...
    case red
    case gold
    case green
}

func gpsStatus(from location: String) -> GPSStatus {
    switch location {
    case "ok":
        return .green
        
    default:
        return .gold
        
    }
}

class UnnamedLocationThingy {
    let locationPublisher = PassthroughSubject<String,Never>()
    var gpsStatus:GPSStatus = .red {
        didSet { print("set the new value to \(String(describing: gpsStatus))") }
    }

    private enum LocationError: Error {
        case timeout
    }
    
    private var statusProvider: AnyCancellable?
    init() {
        statusProvider = makeStatusProvider()
            .assign(to: \.gpsStatus, on: self)
    }
    
    func makeStatusProvider() -> AnyPublisher<GPSStatus, Never> {
        return locationPublisher
            .map(gpsStatus(from:)) //maps CLLocation to GPSStatus enum
            .setFailureType(to: LocationError.self)
            .timeout(.seconds(2), scheduler: DispatchQueue.main) {
                return LocationError.timeout
            }
            .catch { _ in
                self.gpsStatus = .red;
                return self.makeStatusProvider()
            }.eraseToAnyPublisher()
    }
}

let thingy = UnnamedLocationThingy();
Task {
    for delay in [1, 1, 1, 3, 1, 1, 3, 1] {
        try await Task.sleep(for: .seconds(delay))
        thingy.locationPublisher.send("ok")
    }
}

The heart of it is the makeStatusProvider function. This function creates a publisher that will convert published locations to GPSStatus values as long as they come in in a time limit. But if one doesn't come fast enough, it times out. I set up the timeout operator with a customError: handler so that it doesn't just terminate the publisher, but sends an error. I can catch that error in the catch operator and substitute a new publisher to replace the old one. The new publisher I substitute is a brand new publisher created by makeStatusProvider which, as we've just seen is a publisher that converts published locations into GPSStatus values until it hits a timeout...

It's a form of recursion.

I've decorated your code with enough stuff to make a Playground and added a bit of code at the end to exercise the functionality.

  • Related