1

I'm interested in getting notified when the currentMode property of the RunLoop class changes, more specifically, I'm interested in getting an event when the mode is entering .tracking state.

I've tried two different approaches:

This one simply doesn't work, what could be wrong with it?:

import Foundation
public final class RunLoopTracker {
    private let runLoop: RunLoop

    private var observation: NSKeyValueObservation?

    public init(runLoop: RunLoop) {
        self.runLoop = runLoop
    }


    public func attach() {
        observation = runLoop.observe(\.currentMode) { runLoop, change in
            print(change)
        }
    }
}

This one works, but fires only once. I'd like to get the block executed each time the RunLoop enters the specific mode:

import Foundation

public final class RunLoopTracker2 {
    private let runLoop: RunLoop

    private var observation: NSKeyValueObservation?

    public init(runLoop: RunLoop) {
        self.runLoop = runLoop
    }


    public func attach() {
        runLoop.perform(inModes: [.tracking]) {
            print("Entering the tracking mode, send notification")
        }
    }
}


What could be the solution to these two problems or a different approach to track RunLoop.currentMode changes?

Richard Topchii
  • 7,075
  • 8
  • 48
  • 115
  • Did you tried a runLoop extension with a didSet observer on mode ? – Ptit Xav Mar 01 '22 at 14:35
  • That wouldn't work, I'd like to observe the change of the `RunLoop.main`, I couldn't just "subclass" it, since it's provided by the system – Richard Topchii Mar 01 '22 at 20:49
  • I did not wrote subclass : add an extension to current RunLoop class – Ptit Xav Mar 01 '22 at 21:10
  • Could you provide the example? – Richard Topchii Mar 03 '22 at 11:13
  • I checked again and this property does not exists as it in RunLoop definition. BTW I check on internet and it seems that RunLoop can not change. Found [this entry](https://khorbushko.github.io/article/2020/11/29/runloop-in-depth.html) on GitHub . It has one paragraph the state : "RunLoop has few Modes with Source/Timer/Observer in it. Only ONE Mode can be active at once, and it’s called current. To switch between modes u need to exit Loop and set a new mode. Why? just to separate Source/Timer/Observer and make them not affect each other." – Ptit Xav Mar 03 '22 at 18:30

1 Answers1

0

I finished with the following solution:

import Foundation
import Combine

final class RunLoopModeTracker: ObservableObject {
    @Published var isTracking = false

    private let runLoop: RunLoop
    private var taskSet = false

    public init(runLoop: RunLoop = .main) {
        self.runLoop = runLoop
        submitTaskForTrackingMode()
    }


    private func submitTaskForTrackingMode() {
        if !taskSet {
            runLoop.perform(inModes: [.tracking]) { [weak self] in
                self?.notify()
            }
            taskSet = true
        }
    }

    private func notify() {
        isTracking = true
        taskSet = false
        submitTaskForDefaultMode()
    }


    private func submitTaskForDefaultMode() {
        if !taskSet {
            runLoop.perform(inModes: [.default]) { [weak self] in
                guard let self = self else {return}
                self.isTracking = false
                self.submitTaskForTrackingMode()
            }
        }
    }
}

And then at call site I just use it like this:

@StateObject private var runLoopTracker = RunLoopModeTracker()
/// ...
.onChange(of: runLoopTracker.isTracking) { isTracking                 
/// isTracking value has changed to true
}

Essentially, the idea is to add the task for both the default and the tracking modes and once the runloop enters any of those, update the status correspondingly.

Richard Topchii
  • 7,075
  • 8
  • 48
  • 115