NSScreen not updating monitor count when new monitors are plugged in

357 Views Asked by At

I am finding that NSScreen is returning the same number of monitors even after additional monitors are plugged in.

Made a simple test app that can replicate the issue. Basically infinite loops and prints NSScreen counts and CGDisplay counts.

  1. Start the app
  2. printing “NSScreen = 1 CGDisplay = 1”
  3. Without stopping the app, plug in an additional monitor
  4. printing “NSScreen = 1 CGDisplay = 2”

However the code should be printing “NSScreen = 2 CGDisplay = 2”

On OS X 11, we see the same issue (NSScreen = 1 CGDisplay = 2) after plugging in the additional monitor.

Test Code is here:

main.cpp

#include <iostream>
#include "macScreen.h"
int main(int argc, const char * argv[]) {
  getNSScreenCoordinates();
  return 0;
}

macScreen.h

#ifndef macScreen_h
#define macScreen_h
float getNSScreenCoordinates();
#endif /* macScreen_h */

macScreen.mm

#import <Foundation/Foundation.h>
#include <iostream>
#include <Cocoa/Cocoa.h>
#import <AppKit/NSScreen.h>
#define MAX_NUM_DISPLAYS 255
float getNSScreenCoordinates() {

  NSArray<NSScreen *> *arr = [NSScreen screens];
  NSUInteger numScreens = [arr count];

  CGDirectDisplayID displays[MAX_NUM_DISPLAYS];
  CGDisplayCount displayCount;
  CGGetOnlineDisplayList(MAX_NUM_DISPLAYS, displays, &displayCount);
  
  while(1) {
    std::cout << "cg num displays " << displayCount << "\n";
    std::cout << "numscreens " << numScreens << "\n";

    arr = [NSScreen screens];
    numScreens = [arr count];

    CGGetOnlineDisplayList(MAX_NUM_DISPLAYS, displays, &displayCount);
    
  }
  return 1;
}
2

There are 2 best solutions below

2
rob mayoff On

In my testing on macOS 11.6 (Big Sur), you need to do two things to get [NSScreen screens] to update:

  • You need to ensure the singleton NSApplication.shared object exists.
  • You need to run the main NSRunLoop.

Here's an example (in Swift) that works:

import Cocoa
import Combine

// Ensure the singleton NSApplication exists.
_ = NSApplication.shared

// Arrange to print the number of screens once per second,
// while the main RunLoop is running.
let ticket = Timer.publish(every: 1, on: .main, in: .common)
    .autoconnect()
    .sink { _ in
        print("screen count = \(NSScreen.screens.count)")
    }

// Run the main RunLoop forever.
RunLoop.main.run()

// Ensure that the Timer isn't cancelled prematurely.
ticket.cancel()

But if you comment out the NSApplication.shared line, it no longer works. If you replace the use of the RunLoop with a while loop that never invokes the RunLoop, it also no longer works. For example, the following does not work:

import Cocoa
import Combine

// Ensure the singleton NSApplication exists.
_ = NSApplication.shared

while true {
    // NEVER UPDATES
    print("screen count = \(NSScreen.screens.count)")
    sleep(1)
}

So you should really try to arrange your program so that RunLoop.main is in control. But if you really need to have your own main loop, it suffices to run one pass through RunLoop.main before checking NSScreen.screens. This version works:

import Cocoa
import Combine

// Ensure the singleton NSApplication exists.
_ = NSApplication.shared

while true {
    RunLoop.main.acceptInput(forMode: .default, before: .distantPast)
    print("screen count = \(NSScreen.screens.count)")
    sleep(1)
}
0
Thomas Brandstätter On

According to the docs of NSScreen, you need to run your code within an NSApplication instance, to allow NSScreen to connect to the underlaying Display services. Do your work within - (void)applicationDidFinishLaunching:(NSNotification *)aNotification suffice or call NSApplicationLoad before doing anything that requires a DisplayServer connection.

The fact that NSScreen is provided by AppKit indicates such requirements also.

You may consider register to the notification NSApplicationDidChangeScreenParametersNotification or specify a CGDisplayRegisterReconfigurationCallback function.