Search code examples
macosnstimernstextfield

How to prevent mouse event to freeze timers


I have a very simple macOS app that runs a NSTimer to update a value and display it on a NSTextField on my NSView.

The NSView also contains NSButton and NSSlider controls, not related to that timer.

When I hold mouse button on a NSButton or NSSlider, the timer does not update anymore, until I lift the mouse button (and then the timer resume).

How do I prevent the mouse button events to freeze timer?

The timer should not freeze when I hit and hold mouse button on the NSButton and NSSlider controls.

UPDATE:

I've found a solution, I need to subclass any NSButton and NSSlider objects, with empty mouse event functions like this:

  override func mouseUp(with event: NSEvent) {}

(those functions are a lot).

Mouse event will not bother NSTimer anymore.

But this solution is tricky, I'll go for the suggested solution.


Solution

  • It is a question of the run loop modes that you are using for the timer:

    • The “default” run loop modes will not let the timer fire while the user is interacting with the slider (or whatever). E.g., in Objective-C:

      - (void)startUpdatingDefaultRunMode {
          typeof(self) __weak weakSelf = self;
          [NSTimer scheduledTimerWithTimeInterval:0.02 repeats:true block:^(NSTimer * _Nonnull timer) {
              typeof(self) strongSelf = weakSelf;
              if (!strongSelf) {
                  [timer invalidate];
                  return;
              }
              strongSelf.label.stringValue = [strongSelf.dateFormatter stringFromDate:[NSDate now]];
          }];
      }
      

      Or Swift:

      func startUpdatingDefaultRunMode() {
          Timer.scheduledTimer(withTimeInterval: 0.02, repeats: true) { [weak self] timer in
              guard let self else { 
                  timer.invalidate()
                  return
              }
              label.stringValue = dateFormatter.string(from: .now)
          }
      }
      

      Resulting in:

      enter image description here

    • But if you use “common” run loop modes, the timer will continue to fire. In Objective-C:

      - (void)startUpdatingCommonRunMode {
          typeof(self) __weak weakSelf = self;
          NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:0.02 repeats:true block:^(NSTimer * _Nonnull timer) {
              typeof(self) strongSelf = weakSelf;
              if (!strongSelf) {
                  [timer invalidate];
                  return;
              }
              strongSelf.label.stringValue = [strongSelf.dateFormatter stringFromDate:[NSDate now]];
          }];
          [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
      }
      

      Or Swift:

      func startUpdatingCommonRunMode() {
          let timer = Timer(fire: .now, interval: 0.02, repeats: true) { [weak self] timer in
              guard let self else {
                  timer.invalidate()
                  return
              }
              label.stringValue = dateFormatter.string(from: .now)
          }
          RunLoop.main.add(timer, forMode: .common)
      }
      

      Resulting in:

      enter image description here