5

I can specify the scheduler as RunLoop.main, but I could not find a native way to provide the associated RunLoop.Mode mode to receive elements from a publisher.

Why do I need this: I'm updating a tableView cell from my publisher but the UI does not update if the user is scrolling, it then updates as soon as the user interaction or scroll stops. This is a known behaviour for scrollViews but I want my content to be displayed as soon as possible, and being able to specify the run loop tracking mode would fix this.

Combine API: I do not think the receive(on:options:) method have any matching options to provide this. I think internally, if I call receive(on:RunLoop.main) then RunLoop.main.perform { } is called. This perform method can take the mode as parameter but this is not exposed to the Combine API.


Current Idea: To go around this I could do the perform action myself and not use the Combine API, so instead of doing this:

cancellable = stringFuture.receive(on: RunLoop.main) // I cannot specify the mode here
                          .sink { string in
    cell.textLabel.text = string
}

I could do this:

cancellable = stringFuture.sink { string in
    RunLoop.main.perform(inModes: [RunLoop.Mode.common]) { // I can specify it here
        cell.textLabel.text = string
    }
}

But this is not ideal.

Ideal Solution: I was wondering how could I wrap this into my own implementation of a publisher function to have something like this:

cancellable = stringFuture.receive(on: RunLoop.main, inMode: RunLoop.Mode.common)
                          .sink { string in
    cell.textLabel.text = string
}

Were the API of this function could be something like this:

extension Publisher {
    public func receive(on runLoop: RunLoop, inMode: RunLoop.Mode) -> AnyPublisher<Future.Output, Future.Failure> {

        // How to implement this?

    }
}
Ludovic Landry
  • 11,606
  • 10
  • 48
  • 80

2 Answers2

4

Actually what you've requested is custom Scheduler, because RunLoop is a Scheduler and running it in specific mode, instead of .default, is just additional configuration of that scheduler.

I think that Apple will add such possibility in their RunLoop scheduler in some of next updates, but for now the following simple custom scheduler that wraps RunLoop works for me. Hope it would be helpful for you.

Usage:

.receive(on: MyScheduler(runLoop: RunLoop.main, modes: [RunLoop.Mode(rawValue: "myMode")]))

or

.delay(for: 10.0, scheduler: MyScheduler(runLoop: RunLoop.main, modes: [.common]))

Scheduler code:

struct MyScheduler: Scheduler {
    var runLoop: RunLoop
    var modes: [RunLoop.Mode] = [.default]

    func schedule(after date: RunLoop.SchedulerTimeType, interval: RunLoop.SchedulerTimeType.Stride,
                    tolerance: RunLoop.SchedulerTimeType.Stride, options: Never?,
                    _ action: @escaping () -> Void) -> Cancellable {
        let timer = Timer(fire: date.date, interval: interval.magnitude, repeats: true) { timer in
            action()
        }
        for mode in modes {
            runLoop.add(timer, forMode: mode)
        }
        return AnyCancellable {
            timer.invalidate()
        }
    }

    func schedule(after date: RunLoop.SchedulerTimeType, tolerance: RunLoop.SchedulerTimeType.Stride,
                    options: Never?, _ action: @escaping () -> Void) {
        let timer = Timer(fire: date.date, interval: 0, repeats: false) { timer in
            timer.invalidate()
            action()
        }
        for mode in modes {
            runLoop.add(timer, forMode: mode)
        }
    }

    func schedule(options: Never?, _ action: @escaping () -> Void) {
        runLoop.perform(inModes: modes, block: action)
    }

    var now: RunLoop.SchedulerTimeType { RunLoop.SchedulerTimeType(Date()) }
    var minimumTolerance: RunLoop.SchedulerTimeType.Stride { RunLoop.SchedulerTimeType.Stride(0.1) }

    typealias SchedulerTimeType = RunLoop.SchedulerTimeType
    typealias SchedulerOptions = Never
}
Asperi
  • 228,894
  • 20
  • 464
  • 690
0

You can pass DispatchQueue.main to the receive(on:options:) because DispatchQueue also comforms to Scheduler protocol.

Somehow it makes deliver events while scrolling.

Like the following:

cancellable = stringFuture.receive(on: DispatchQueue.main)
                          .sink { string in
    cell.textLabel.text = string
}

kishikawa katsumi
  • 10,418
  • 1
  • 41
  • 53
  • that doesn't allow specifying the run mode though. Default mode is not processed during a gesture tracking event (such as when scrolling, while your finger is down) – pqnet May 20 '20 at 17:11