HangarRash
HangarRash

Reputation: 15058

How to respond to the new Text Size setting in macOS 14 Sonoma?

Summary

macOS 14 Sonoma adds the "Text Size" setting under Settings -> Accessibility -> Display. This is similar to the "Text Size" setting that has been in iOS for quite some time.

Many Apple apps automatically adjust the size of their text based on this setting. This includes Xcode, Settings, Finder, Mail, Notes, and others. Apps like Xcode and Settings only change the text in sidebars while the other apps change text throughout the app.

Question

My question is how to respond to this setting in my own macOS app?

Research

I've searched the AppKit documentation. I've searched WWDC videos. I've searched the Apple developer forums. There's no mention of how this is done that I have seen.

Here on SO I've seen:

Experiments

I've created a native macOS app using AppKit. I've added an NSTextField and an NSTextView and set the fonts to a text style such as "Title 1". Changing the setting does not automatically change the displayed font size in the app.

I've created a SwiftUI app and added a native macOS build. I setup a ContentView with some Text using Text("This is some text").font(.title). Changing the setting does not automatically change the displayed font size in the app.

I have an iOS app built for macOS using Mac Catalyst. The iOS version of the app automatically adjusts its text when the "Text Size" setting is changed in the iOS Settings app. The macOS version of the app does not change at all when the "Text Size" setting is changed in the macOS Settings app.

In the native AppKit app I tried the following code in the AppDelegate:

NSWorkspace.shared.notificationCenter.addObserver(forName: nil, object: nil, queue: nil) { note in
    print(note)
}
NotificationCenter.default.addObserver(forName: nil, object: nil, queue: nil) { note in
    print(note)
}

Neither of these generated any output when the "Text Size" setting was changed. Under iOS/UIKit, you get a UIContentSizeCategory.didChangeNotification notification.

As a shot in the dark I setup the AppKit app with my own custom NSApplication subclass and added the following:

class Application: NSApplication {
    override func sendEvent(_ event: NSEvent) {
        print("event \(event)")
        super.sendEvent(event)
    }

    override func sendAction(_ action: Selector, to target: Any?, from sender: Any?) -> Bool {
        print("action: \(action)")
        return super.sendAction(action, to: target, from: sender)
    }
}

Neither of these produced output when the "Text Size" setting was changed.

Summary

I'm at a loss at this point. What code is needed to allow a macOS app to respond to changes to the "Text Size" setting? My immediate need is with an iOS/UIKit app built for macOS with Mac Catalyst but I'm fine with solutions in native AppKit or even SwiftUI. Code can be in either Swift or Objective-C.

Upvotes: 3

Views: 1280

Answers (1)

ix4n33
ix4n33

Reputation: 586

Getting Text Size with defaults

Text size are stored in com.apple.universalaccess.plist as FontSizeCategory:

$ defaults read com.apple.universalaccess FontSizeCategory

# {
#     "com.apple.MobileSMS" = UseGlobal;
#     "com.apple.Notes" = UseGlobal;
#     "com.apple.finder" = UseGlobal;
#     "com.apple.iBooksX" = UseGlobal;
#     "com.apple.iCal" = UseGlobal;
#     "com.apple.mail" = UseGlobal;
#     "com.apple.news" = UseGlobal;
#     "com.apple.stocks" = UseGlobal;
#     "com.apple.weather" = UseGlobal;
#     global = DEFAULT;
#     version = "3.0";
# }

global is what we're interested in.

Here are a list of possible values and corresponding options in the Settings app:

Value Text Size Sidebar Icon Size
XXXS 9pt Small
XXS 10pt Small
XS 11pt Small
S 12pt Medium
DEFAULT Default (11pt) Medium
M 13pt Medium
L 14pt Medium
XL 15pt Large
XXL 16pt Large
XXXL 17pt Large
AX1 20pt Large
AX2 24pt Large
AX3 29pt Large
AX4 35pt Large
AX5 42pt Large

Observing Text Size Changes in Code

To observe text size changes:

  1. Create a UserDefaults object with suite name com.apple.universalaccess. Make sure you keep a strong reference to it, or you're not getting KVO callbacks.

  2. Observe changes for key FontSizeCategory with KVO.

extension UserDefaults {
    // I'm using `observe(_:options:changeHandler:)` below and it only accept 
    // a `KeyPath` key path, so this is required for this use case. 
    // Notice that the property name is the same as the key `FontSizeCategory`, 
    // because the property name is used for KVO key.
    @objc var FontSizeCategory: [String: Any]? {
        dictionary(forKey: "FontSizeCategory")
    }
}

let universalAccessDefaults = UserDefaults(suiteName: "com.apple.universalaccess")!
var fontSizeCategoryObservation: NSKeyValueObservation?

fontSizeCategoryObservation = universalAccessDefaults.observe(\.FontSizeCategory, options: .new) {  _, changes in
    guard 
      let newValue = changes.newValue.flatMap(\.self), 
      let global = newValue["global"] as? String 
    else { return }

    /* Update your UI for text size change */
}

Some said that this requires a temporary exception entitlement and is usually not allowed for App Store.

Getting Scaled Font Size for Text Size

Unfortunately, we don't have dynamic type in macOS APIs yet. Methods like NSFont.preferredFont(forTextStyle:) always return a fixed font size.

For now, we would need to maintain a list of font size our own, migrated from iOS. This is a lot of works, and I barely know anything about typography, so I'm leaving this to you.

Here are some links that might be useful:

Upvotes: 2

Related Questions