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.