How to read the physical screen size of OSX?

You can use CGDisplayScreenSize to get the physical size of a screen in millimetres. From that you can compute the DPI given that you already know the resolution.

So e.g.

NSScreen *screen = [NSScreen mainScreen];
NSDictionary *description = [screen deviceDescription];
NSSize displayPixelSize = [[description objectForKey:NSDeviceSize] sizeValue];
CGSize displayPhysicalSize = CGDisplayScreenSize(
            [[description objectForKey:@"NSScreenNumber"] unsignedIntValue]);

NSLog(@"DPI is %0.2f", 
         (displayPixelSize.width / displayPhysicalSize.width) * 25.4f); 
         // there being 25.4 mm in an inch

That @"NSScreenNumber" thing looks dodgy but is the explicit documented means of obtaining a CGDirectDisplayID from an NSScreen.


Tommy’s answer above is excellent — I’ve ported it to Swift (for my own use) and am posting that here as a reference, but Tommy’s should be consider canonical.

import Cocoa

public extension NSScreen {
    var unitsPerInch: CGSize {
        let millimetersPerInch:CGFloat = 25.4
        let screenDescription = deviceDescription
        if let displayUnitSize = (screenDescription[NSDeviceDescriptionKey.size] as? NSValue)?.sizeValue,
            let screenNumber = (screenDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber)?.uint32Value {
            let displayPhysicalSize = CGDisplayScreenSize(screenNumber)
            return CGSize(width: millimetersPerInch * displayUnitSize.width / displayPhysicalSize.width,
                          height: millimetersPerInch * displayUnitSize.height / displayPhysicalSize.height)
        } else {
            return CGSize(width: 72.0, height: 72.0) // this is the same as what CoreGraphics assumes if no EDID data is available from the display device — https://developer.apple.com/documentation/coregraphics/1456599-cgdisplayscreensize?language=objc
        }
    }
}

if let screen = NSScreen.main {
    print("main screen units per inch \(screen.unitsPerInch)")
}

Please note that the value returned is kind of effectively the ‘points per inch’ (but not for all definitions; see below) and almost never the ‘pixels per inch’ — modern Macs have a number of pixels per point that depends on the current “Resolution” setting in System Preferences and the inherent resolution of the device (Retina displays have a lot more pixels).

What you do know about the return value is that if you draw a line with code like

CGRect(origin: .zero, size: CGSize(width: 10, height: 1)).fill()

the line will be 1 / pointsPerInch.height inches high and 1 / pointsPerInch.width inches wide if you measure it with a very precise ruler held up to your screen.

(For a long time graphics frameworks have defined a ‘point’ as both “1/72nd of an inch in the real world” and also as “whatever the width or height of a box that’s 1 x 1 units ends up being on the current monitor at the current resolution — two definitions that are usually in conflict with each other.)

So for this code I use the word ‘unit’ to make it clear we’re not dealing with 1/72nd of an inch, nor 1 physical pixel.