Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to specify the run loop & mode to to receive elements from a publisher

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?

    }
}
like image 220
Ludovic Landry Avatar asked Nov 27 '19 01:11

Ludovic Landry


People also ask

What is run loop mode?

A run loop mode is a collection of input sources and timers to be monitored and a collection of run loop observers to be notified. Each time you run your run loop, you specify (either explicitly or implicitly) a particular “mode” in which to run.

What is a run loop Swift?

A RunLoop is a programmatic interface to objects that manage input sources, such as touches for an application. A RunLoop is created and managed by the system, who's also responsible for creating a RunLoop object for each thread object.


1 Answers

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
}
like image 108
Asperi Avatar answered Oct 19 '22 02:10

Asperi