Damien Pontifex
Damien Pontifex

Reputation: 1262

Swift combine publishers where one hasn't sent a value yet

I have a publisher which would need re-evaluating on day change, but should continue to emit values at any other time.

As such, I thought I could use a NotificationCenter publisher for the UIApplication.significantTimeChangeNotification notification and combine it with my publisher such that the combine emission process would re-run on either on data change or day change and hence re-evaluate the map filter. See a rough outline of that code below.

The problem is that there is no published event by NotificationCenter at the point in time that this is setup and hence, none of the following map etc calls actually evaluate. merge(with:) won't work as the two publishers publish different types, but combineLatest(_:) and zip(_:) both won't emit events until both publishers have emitted a single event.

I can validate that my code operates as expected by adding NotificationCenter.default.post(name: UIApplication.significantTimeChangeNotification, object: nil) after this code, but that is undesirable due to it potentially signalling other areas of the app that an actual time change has occurred when it hasn't

private func todaysDate() -> String {
    let formatter = DateFormatter()
    formatter.dateFormat = "YYYY-MM-dd"
    return formatter.string(from: Date())
}

@Published var entities: [MyEntity]

let dayChangePublisher = NotificationCenter.default
    .publisher(for: UIApplication.significantTimeChangeNotification)

$entities.combineLatest(dayChangePublisher)
    .map(\.0) // Only pass on the entity for further operations
    .map { entities -> MyEntity? in
        let today = todaysDate()
        return entities?.first(where: { $0.id == today })
    }
    ...remainder of combine code

Can this combination of publishers and evaluation of events occur with the current Swift combine framework? Like the behaviour I'd expect from merge(with:) but where the publishers emit two different types.

edit: I found one solution where I map the notification publisher to a nil array

let dayChangePublisher = NotificationCenter.default
    .publisher(for: UIApplication.significantTimeChangeNotification)
    .map { _ ➝ [MyEntity]? in
        return nil
    }

And then use merge and compactMap to avoid passing any nil values on

let mergedPub = repo.$entities
        .merge(with: dayChangePublisher)
        .compactMap { entity -> MyEntity? in
            let today = todaysDate()
            return entities?.first { $0.id == today }
        }
        .share()

It works, but maybe a bit cumbersome if anyone has a better solution?

Upvotes: 3

Views: 2494

Answers (1)

New Dev
New Dev

Reputation: 49590

If I understood your question, you need a combineLatest that is not blocked by not having an initial value from one of the publishers.

You can achieve that with .prepend(value) operator. In this case, since you don't care about the actual value, map first to Void, then prepend a Void. It would work like so:

let dayChangePublisher = NotificationCenter.default
    .publisher(for: UIApplication.significantTimeChangeNotification)

$entities.combineLatest(
   dayChangePublisher
      .map { _ in }
      .prepend(()) // make sure to prepend a () value
    )
    .map(\.0) // Only pass on the entity for further operations
    .map { entities -> MyEntity? in
        let today = todaysDate()
        return entities?.first(where: { $0.id == today })
    }
    //... 

Upvotes: 6

Related Questions