15

I did small app to allow quickly change screen resolutions on multiple monitors. I want to show product name as title of the monitor, and it's very simple to find using this code:

NSDictionary *deviceInfo = (__bridge NSDictionary *)IODisplayCreateInfoDictionary(CGDisplayIOServicePort(dispID), kIODisplayOnlyPreferredName);

NSDictionary *localizedNames = [deviceInfo objectForKey:[NSString stringWithUTF8String:kDisplayProductName]];

if([localizedNames count] > 0) {
    _title = [localizedNames objectForKey:[[localizedNames allKeys] objectAtIndex:0]];
} else {
    _title = @"Unknown display";
}

But CGDisplayIOServicePort is deprecated in OS X >= 10.9 and Apple's documentation says there is no replacement. How to find service port or product name without using this method?

I tried to iterate through IO-registry and tried to use IOServiceGetMatchingServices method to find display services but I'm not very familiar with IO-registry so I couldn't find solution.

Thanks for help!

juniperi
  • 3,723
  • 1
  • 23
  • 30
  • 3
    I'd recommend filing a radar against this. Apple should tell you how to achieve what you're trying to do. – pmdj Nov 18 '13 at 20:09
  • Did you find a solution to this problem? – Frog May 14 '14 at 16:25
  • @Frog Nope. If Apple don't introduce any new APIs in WWDC to solve this, maybe it's time to open radar ticket for this. – juniperi May 14 '14 at 19:58
  • 2
    Just a note: the method, while being deprecated, worked flawlessly (been shipping it since 10.6.~) all these years and still works on macOS 10.15.1 – Kentzo Nov 10 '19 at 07:51

4 Answers4

3

It looks like @Eun's post missed a piece of information to close this discussion. With a little search, I found that IOServicePortFromCGDisplayID is not an API which Apple provides. Rather, it's a piece of open source code found here: https://github.com/glfw/glfw/blob/e0a6772e5e4c672179fc69a90bcda3369792ed1f/src/cocoa_monitor.m

I copied IOServicePortFromCGDisplayID and also 'getDisplayName' from it. I needed two tweaks to make it work on OS X 10.10.

  1. Remove the code to handle serial number in IOServicePortFromCGDisplayID. (CFDictionaryGetValue for kDisplaySerialNumber returns NULL for me.)
  2. Remove project specific error handling code in getDisplayName.

If you need more information

  • Issue tracker of the problem: github.com/glfw/glfw/issues/165
  • Commit for the solution: github.com/glfw/glfw/commit/e0a6772e5e4c672179fc69a90bcda3369792ed1f

I would thank Matthew Henry who submitted the code there.

Hiroshi
  • 71
  • 1
  • 5
  • Thanks for the solution! I just tested this and it works. – juniperi Nov 13 '15 at 20:50
  • 1
    Unfortunately this will break if you have multiple identical displays connected. If you only match the vendor and product numbers, the first one to show up in the IOServices will always be returned. It's a general problem with the approach taken by the code linked to above—it tries to match vendor id, product id, and serial number, but not all displays return serial numbers (projectors generally don't, for example). – Siobhán Apr 16 '18 at 19:14
  • i used it here https://stackoverflow.com/questions/17305184/programmatically-change-display-rotation-on-mac-os-using-xcode for rotating the screen and it does not work. Any idea about that could be the issue. Thanks – last-Programmer Apr 15 '21 at 11:38
3

Here is my take on the issue. I also started with the code from GLFW 3.1, file cocoa_monitor.m.
But I had to modify it in different ways than Hiroshi said, so here goes:

// Get the name of the specified display
- (NSString*) screenNameForDisplay: (NSNumber*) screen_id
{
    CGDirectDisplayID displayID = [screen_id unsignedIntValue];

    io_service_t serv = [self IOServicePortFromCGDisplayID: displayID];
    if (serv == 0)
        return @"unknown";

    CFDictionaryRef info = IODisplayCreateInfoDictionary(serv, kIODisplayOnlyPreferredName);
    IOObjectRelease(serv);

    CFStringRef display_name;
    CFDictionaryRef names = CFDictionaryGetValue(info, CFSTR(kDisplayProductName));

    if ( !names ||
         !CFDictionaryGetValueIfPresent(names, CFSTR("en_US"), (const void**) & display_name)  )
    {
        // This may happen if a desktop Mac is running headless
        CFRelease( info );
        return @"unknown";
    }

    NSString * displayname = [NSString stringWithString: (__bridge NSString *) display_name];
    CFRelease(info);
    return displayname;
}


// Returns the io_service_t (an int) corresponding to a CG display ID, or 0 on failure.
// The io_service_t should be released with IOObjectRelease when not needed.

- (io_service_t) IOServicePortFromCGDisplayID: (CGDirectDisplayID) displayID
{
    io_iterator_t iter;
    io_service_t serv, servicePort = 0;

    CFMutableDictionaryRef matching = IOServiceMatching("IODisplayConnect");

    // releases matching for us
    kern_return_t err = IOServiceGetMatchingServices( kIOMasterPortDefault, matching, & iter );
    if ( err )
        return 0;

    while ( (serv = IOIteratorNext(iter)) != 0 )
    {
        CFDictionaryRef displayInfo;
        CFNumberRef vendorIDRef;
        CFNumberRef productIDRef;
        CFNumberRef serialNumberRef;

        displayInfo = IODisplayCreateInfoDictionary( serv, kIODisplayOnlyPreferredName );

        Boolean success;
        success =  CFDictionaryGetValueIfPresent( displayInfo, CFSTR(kDisplayVendorID),  (const void**) & vendorIDRef );
        success &= CFDictionaryGetValueIfPresent( displayInfo, CFSTR(kDisplayProductID), (const void**) & productIDRef );

        if ( !success )
        {
            CFRelease(displayInfo);
            continue;
        }

        SInt32 vendorID;
        CFNumberGetValue( vendorIDRef, kCFNumberSInt32Type, &vendorID );
        SInt32 productID;
        CFNumberGetValue( productIDRef, kCFNumberSInt32Type, &productID );

        // If a serial number is found, use it.
        // Otherwise serial number will be nil (= 0) which will match with the output of 'CGDisplaySerialNumber'
        SInt32 serialNumber = 0;
        if ( CFDictionaryGetValueIfPresent(displayInfo, CFSTR(kDisplaySerialNumber), (const void**) & serialNumberRef) )
        {
            CFNumberGetValue( serialNumberRef, kCFNumberSInt32Type, &serialNumber );
        }

        // If the vendor and product id along with the serial don't match
        // then we are not looking at the correct monitor.
        // NOTE: The serial number is important in cases where two monitors
        //       are the exact same.
        if( CGDisplayVendorNumber(displayID) != vendorID ||
            CGDisplayModelNumber(displayID)  != productID ||
            CGDisplaySerialNumber(displayID) != serialNumber )
        {
            CFRelease(displayInfo);
            continue;
        }

        servicePort = serv;
        CFRelease(displayInfo);
        break;
    }

    IOObjectRelease(iter);
    return servicePort;
}

This works fine for me in a screensaver I wrote under macOS 10.11 (El Capitan). I tested it with the built-in display of my MacBookPro and an Apple Display connected via Thunderbolt.

Gab
  • 306
  • 3
  • 7
  • I think there is a leak in this code. The loop in `IOServicePortFromCGDisplayID` creates a number of values for the `serv` variable. The final one is passed to `IOObjectRelease` in the caller, but any earlier non-matching ones are not released. – JWWalker Apr 10 '18 at 21:24
2

As of macOS 10.15 -[NSScreen localizedName] is available:

NSLog(@"Name of main display is %@", NSScreen.mainScreen.localizedName);
Dimitar Nestorov
  • 2,411
  • 24
  • 29
-1
NSString* screenNameForDisplay(CGDirectDisplayID displayID)
{
    NSString *screenName = nil;
    io_service_t service = IOServicePortFromCGDisplayID(displayID);
    if (service)
    {
        NSDictionary *deviceInfo = (NSDictionary *)IODisplayCreateInfoDictionary(service, kIODisplayOnlyPreferredName);
        NSDictionary *localizedNames = [deviceInfo objectForKey:[NSString stringWithUTF8String:kDisplayProductName]];

        if ([localizedNames count] > 0) {
            screenName = [[localizedNames objectForKey:[[localizedNames allKeys] objectAtIndex:0]] retain];
        }

        [deviceInfo release];
    }
    return [screenName autorelease];
}
Eun
  • 4,146
  • 5
  • 30
  • 51
  • I tried that, but I'm getting error when trying to use IOServicePortFromCGDisplayID, and can't find any documentation for that. – juniperi Jun 18 '14 at 11:01
  • can you specify which error? are you sure the displayID is correct? It works for me [here](https://github.com/Eun/DisableMonitor/blob/gui/DisableMonitorAppDelegate.m#L393). – Eun Jun 18 '14 at 11:23
  • warning: implicit declaration of function + error: Undefined symbols for architecture x86_64:"_IOServicePortFromCGDisplayID" – juniperi Jun 18 '14 at 11:29
  • add the prototype to your header: `extern io_service_t IOServicePortFromCGDisplayID(CGDirectDisplayID displayID);` – Eun Jun 18 '14 at 11:33