Search code examples
iosswiftkey-value-observingnsrunlooprunloop

Swift RunLoop: get notified on currentMode change


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?


Solution

  • 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.