Skip to content

refactor: Use UNUserNotificationCenter instead of NSUserNotificationCenter, fixes sound on macOS 14+#322

Open
billdaws wants to merge 6 commits intojulienXX:masterfrom
billdaws:feature/unusernotificationcenter
Open

refactor: Use UNUserNotificationCenter instead of NSUserNotificationCenter, fixes sound on macOS 14+#322
billdaws wants to merge 6 commits intojulienXX:masterfrom
billdaws:feature/unusernotificationcenter

Conversation

@billdaws
Copy link
Copy Markdown

@billdaws billdaws commented Apr 4, 2026

... Also some minor fixes/cleanups.

Hello, and thank you for the excellent project.

NSUserNotificationCenter was deprecated in macOS 11 and broke sound playback on macOS 14+.

Disclaimer: I know very little about Objective C and macOS application development. Just took a crack at fixing this for my personal use and thought I should upstream it. Could easily be missing some conventions -- if so, happy to update.

Changes

Core migration (AppDelegate.m, AppDelegate.h, project.pbxproj)

  • Replaced NSUserNotificationCenter with UNUserNotificationCenter throughout
  • Added UserNotifications.framework linkage; raised MACOSX_DEPLOYMENT_TARGET from 10.10 -> 10.14 (the minimum required by UNUserNotificationCenter)
  • Removed the NSBundle (FakeBundleIdentifier) method-swizzling category and InstallFakeBundleIdentifierHook() - no longer needed
  • Authorization is now requested explicitly via requestAuthorizationWithOptions: before delivering a notification
  • Notification content is built with UNMutableNotificationContent; sound is set via UNNotificationSound, fixing playback on macOS 14+
  • Image attachments use UNNotificationAttachment (local files only - remote URLs are not supported by this API)
  • Do Not Disturb bypass (-ignoreDnD) uses UNNotificationInterruptionLevelTimeSensitive, available on macOS 12+
    • This doesn't quite work, see below.
  • Notification delivery, removal, and listing are all async, using completion handlers
  • Click handling is implemented via UNUserNotificationCenterDelegate (didReceiveNotificationResponse:) rather than a launch-time notification key
  • applicationWillFinishLaunching: sets the delegate early so click responses are received before applicationDidFinishLaunching: runs

Removed options

  • -sender: not supported by UNUserNotificationCenter; removed from help and code
    • My understanding is that UNUser... uses the bundle ID for notification permissions, so the swizzling that was in use here can't really be maintained without likely breaking authn/z. Since -sender only worked because of that, I dropped it here.
  • -appIcon: not supported by UNUserNotificationCenter; removed from help and code
    • I don't think there's an equivalent for this in UNUser..., and it seems to have only worked because of the same bundle swizzling, so I dropped it.

Ignoring Do Not Disturb

-ignoreDnD doesn't really work as expected in my testing. This project would need entitlements for com.apple.developer.usernotifications.time-sensitive. That requires a paid Apple dev account, which I don't have. I'm not sure if anyone else can test it, but I'm happy to iterate if they do, and it doesn't work.

Housekeeping

  • Removed unused initializeUserDefaults class method
  • Removed unused decodeFragmentInURL:fragment: method
  • Updated executeShellCommand: to use non-deprecated NSTask API (executableURL, launchAndReturnError:)
  • Fixed typo in log message ("indentifier" → "identifier")
  • Removed redundant [NSData dataWithData:] wrapping stdin read
  • Updated help text to reflect removed options and corrected sound path (/System/Library/Sounds)

Testing

Tested manually on macOS 15.5 (Sequoia).

Built with xcodebuild (Release configuration), copied the resulting .app to /Applications/ (for permissions' sake), and ran:

/Applications/my_terminal_notifier.app/Contents/MacOS/terminal-notifier -message 'Test' -sound default

The notification appeared and the default sound played. Worth noting that alert sounds needed to be enabled, and notifications from my terminal permitted, etc.

Tested with some other sounds and they worked.

-contentImage, -title, and -subtitle also work. Didn't test all combinations of flags.

Happy to record a video if it helps.

billdaws added 6 commits April 3, 2026 18:00
NSUserNotification and NSUserNotificationCenter were deprecated in
macOS 11 and removed entirely in macOS 14 (Sonoma), breaking all
notification delivery including the -sound option on modern systems.

Migrates to the UserNotifications framework (UNUserNotificationCenter),
which is the supported API from macOS 10.14 onwards. Raises the minimum
deployment target from 10.10 to 10.14 accordingly.

Changes:
- Fix -sound: now uses UNNotificationSound defaultSound/soundNamed:
- -group now uses UNNotificationRequest identifiers for deduplication
- -remove and -list use async UNUserNotificationCenter equivalents
- -contentImage uses UNNotificationAttachment (local files only)
- -ignoreDnD uses interruptionLevel .timeSensitive on macOS 12+
- Notification permission is requested via requestAuthorizationWithOptions:
- applicationWillFinishLaunching: sets delegate early to handle
  click-launched activations before applicationDidFinishLaunching: runs

Dropped features (no public API equivalent in UNUserNotificationCenter):
- -sender: bundle identifier spoofing via NSBundle swizzle no longer
  has any effect; emits a warning and continues
- -appIcon: _identityImage was a private NSUserNotification API with no
  UNUserNotificationCenter equivalent; emits a warning and continues

Bumps version to 3.0.0.
@billdaws billdaws marked this pull request as ready for review April 4, 2026 06:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant