From ba7c0b897bda025a7d10d46de3b92ee4e8bb58bf Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Mon, 6 Apr 2026 12:46:29 -0400
Subject: [PATCH] Add the Cocoa notification driver
Supported on macOS 10.14+ and iOS.
---
Xcode/SDL/SDL.xcodeproj/project.pbxproj | 44 +++
src/SDL.c | 5 +
src/notification/SDL_notification_c.h | 4 +
.../cocoa/SDL_cocoanotification.m | 360 ++++++++++++++++++
.../dummy/SDL_dummynotification.c | 7 +
5 files changed, 420 insertions(+)
create mode 100644 src/notification/cocoa/SDL_cocoanotification.m
diff --git a/Xcode/SDL/SDL.xcodeproj/project.pbxproj b/Xcode/SDL/SDL.xcodeproj/project.pbxproj
index 33664dcc3091e..6712bb71c1add 100644
--- a/Xcode/SDL/SDL.xcodeproj/project.pbxproj
+++ b/Xcode/SDL/SDL.xcodeproj/project.pbxproj
@@ -58,6 +58,9 @@
1485C3312BBA4AF30063985B /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1485C32F2BBA4A0C0063985B /* UniformTypeIdentifiers.framework */; platformFilters = (maccatalyst, macos, ); settings = {ATTRIBUTES = (Weak, ); }; };
3AFD09EA2F9766BA00208BA9 /* SDL_CurvedUIShader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AFD09E92F9766BA00208BA9 /* SDL_CurvedUIShader.swift */; platformFilters = (xros, ); };
557D0CFA254586CA003913E3 /* CoreHaptics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F37DC5F225350EBC0002E6F7 /* CoreHaptics.framework */; platformFilters = (ios, maccatalyst, macos, tvos, xros, ); settings = {ATTRIBUTES = (Weak, ); }; };
+ 30840D4C2F76A822000F1D1B /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 30840D4B2F76A822000F1D1B /* UserNotifications.framework */; };
+ 30840D4E2F76A8E7000F1D1B /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 30840D4D2F76A8E7000F1D1B /* Security.framework */; };
+ 557D0CFA254586CA003913E3 /* CoreHaptics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F37DC5F225350EBC0002E6F7 /* CoreHaptics.framework */; platformFilters = (ios, maccatalyst, macos, tvos, ); settings = {ATTRIBUTES = (Weak, ); }; };
557D0CFB254586D7003913E3 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A75FDABD23E28B6200529352 /* GameController.framework */; settings = {ATTRIBUTES = (Required, ); }; };
5616CA4C252BB2A6005D5928 /* SDL_url.c in Sources */ = {isa = PBXBuildFile; fileRef = 5616CA49252BB2A5005D5928 /* SDL_url.c */; };
5616CA4D252BB2A6005D5928 /* SDL_sysurl.h in Headers */ = {isa = PBXBuildFile; fileRef = 5616CA4A252BB2A6005D5928 /* SDL_sysurl.h */; };
@@ -566,6 +569,12 @@
F3FD042E2C9B755700824C4C /* SDL_hidapi_nintendo.h in Headers */ = {isa = PBXBuildFile; fileRef = F3FD042C2C9B755700824C4C /* SDL_hidapi_nintendo.h */; };
F3FD042F2C9B755700824C4C /* SDL_hidapi_steam_hori.c in Sources */ = {isa = PBXBuildFile; fileRef = F3FD042D2C9B755700824C4C /* SDL_hidapi_steam_hori.c */; };
FA73671D19A540EF004122E4 /* CoreVideo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA73671C19A540EF004122E4 /* CoreVideo.framework */; platformFilters = (ios, maccatalyst, macos, tvos, xros, ); settings = {ATTRIBUTES = (Required, ); }; };
+ FA73671D19A540EF004122E4 /* CoreVideo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA73671C19A540EF004122E4 /* CoreVideo.framework */; platformFilters = (ios, maccatalyst, macos, tvos, ); settings = {ATTRIBUTES = (Required, ); }; };
+ 00009183ED11C92F23FC0000 /* SDL_notification.c in Sources */ = {isa = PBXBuildFile; fileRef = 000059D16599F687D87B0000 /* SDL_notification.c */; };
+ 0000B6DBAE1F178E87010000 /* SDL_notification_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 000045BF87FD2865AB1C0000 /* SDL_notification_c.h */; };
+ 0000FB5C9B8CE5929A250000 /* SDL_cocoanotification.m in Sources */ = {isa = PBXBuildFile; fileRef = 000088B5AFB11E40F7920000 /* SDL_cocoanotification.m */; };
+ 0000AF1D2CED20010C2D0000 /* SDL_notificationevents.c in Sources */ = {isa = PBXBuildFile; fileRef = 0000D6CCB566B8AE47FE0000 /* SDL_notificationevents.c */; };
+ 00004A19C923458228C10000 /* SDL_notificationevents_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 0000FA7334391F6720820000 /* SDL_notificationevents_c.h */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -624,6 +633,8 @@
02D6A1C128A84B8F00A7F001 /* SDL_hidapi_sinput.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_sinput.c; sourceTree = "<group>"; };
1485C32F2BBA4A0C0063985B /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
3AFD09E92F9766BA00208BA9 /* SDL_CurvedUIShader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDL_CurvedUIShader.swift; sourceTree = "<group>"; };
+ 30840D4B2F76A822000F1D1B /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; };
+ 30840D4D2F76A8E7000F1D1B /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
5616CA49252BB2A5005D5928 /* SDL_url.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_url.c; sourceTree = "<group>"; };
5616CA4A252BB2A6005D5928 /* SDL_sysurl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_sysurl.h; sourceTree = "<group>"; };
5616CA4B252BB2A6005D5928 /* SDL_sysurl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDL_sysurl.m; sourceTree = "<group>"; };
@@ -1168,6 +1179,11 @@
F59C710600D5CB5801000001 /* SDL.info */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = text; path = SDL.info; sourceTree = "<group>"; };
F5A2EF3900C6A39A01000001 /* BUGS.txt */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = text; name = BUGS.txt; path = ../../BUGS.txt; sourceTree = SOURCE_ROOT; };
FA73671C19A540EF004122E4 /* CoreVideo.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreVideo.framework; path = System/Library/Frameworks/CoreVideo.framework; sourceTree = SDKROOT; };
+ 000059D16599F687D87B0000 /* SDL_notification.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = SDL_notification.c; path = SDL_notification.c; sourceTree = "<group>"; };
+ 000045BF87FD2865AB1C0000 /* SDL_notification_c.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDL_notification_c.h; path = SDL_notification_c.h; sourceTree = "<group>"; };
+ 000088B5AFB11E40F7920000 /* SDL_cocoanotification.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SDL_cocoanotification.m; path = SDL_cocoanotification.m; sourceTree = "<group>"; };
+ 0000D6CCB566B8AE47FE0000 /* SDL_notificationevents.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = SDL_notificationevents.c; path = SDL_notificationevents.c; sourceTree = "<group>"; };
+ 0000FA7334391F6720820000 /* SDL_notificationevents_c.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDL_notificationevents_c.h; path = SDL_notificationevents_c.h; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -1176,11 +1192,13 @@
buildActionMask = 2147483647;
files = (
1485C3312BBA4AF30063985B /* UniformTypeIdentifiers.framework in Frameworks */,
+ 30840D4E2F76A8E7000F1D1B /* Security.framework in Frameworks */,
A7381E971D8B6A0300B177DD /* AudioToolbox.framework in Frameworks */,
00D0D0D810675E46004B05EF /* Carbon.framework in Frameworks */,
007317A40858DECD00B2BC32 /* Cocoa.framework in Frameworks */,
A7381E961D8B69D600B177DD /* CoreAudio.framework in Frameworks */,
557D0CFA254586CA003913E3 /* CoreHaptics.framework in Frameworks */,
+ 30840D4C2F76A822000F1D1B /* UserNotifications.framework in Frameworks */,
00D0D08410675DD9004B05EF /* CoreFoundation.framework in Frameworks */,
FA73671D19A540EF004122E4 /* CoreVideo.framework in Frameworks */,
00CFA89D106B4BA100758660 /* ForceFeedback.framework in Frameworks */,
@@ -1445,6 +1463,7 @@
F3E5A6EA2AD5E0E600293D83 /* SDL_properties.c */,
F386F6E62884663E001840AA /* SDL_utils.c */,
F386F6E52884663E001840AA /* SDL_utils_c.h */,
+ 0000A12B47E1FC5391780000 /* notification */,
);
name = "Library Source";
path = ../../src;
@@ -1474,6 +1493,8 @@
564624341FF821B70074AC87 /* Frameworks */ = {
isa = PBXGroup;
children = (
+ 30840D4D2F76A8E7000F1D1B /* Security.framework */,
+ 30840D4B2F76A822000F1D1B /* UserNotifications.framework */,
1485C32F2BBA4A0C0063985B /* UniformTypeIdentifiers.framework */,
F382339B2738ED6600F7F527 /* CoreBluetooth.framework */,
F376F7272559B77100CFC0BC /* CoreAudio.framework */,
@@ -2330,6 +2351,8 @@
A7D8A93723E2514000DCD162 /* SDL_touch_c.h */,
A7D8A92F23E2514000DCD162 /* SDL_windowevents.c */,
A7D8A94323E2514000DCD162 /* SDL_windowevents_c.h */,
+ 0000D6CCB566B8AE47FE0000 /* SDL_notificationevents.c */,
+ 0000FA7334391F6720820000 /* SDL_notificationevents_c.h */,
);
path = events;
sourceTree = "<group>";
@@ -2551,6 +2574,24 @@
path = resources;
sourceTree = "<group>";
};
+ 0000A12B47E1FC5391780000 /* notification */ = {
+ isa = PBXGroup;
+ children = (
+ 000059D16599F687D87B0000 /* SDL_notification.c */,
+ 000045BF87FD2865AB1C0000 /* SDL_notification_c.h */,
+ 0000B071CC4D6AB5CE640000 /* cocoa */,
+ );
+ path = notification;
+ sourceTree = "<group>";
+ };
+ 0000B071CC4D6AB5CE640000 /* cocoa */ = {
+ isa = PBXGroup;
+ children = (
+ 000088B5AFB11E40F7920000 /* SDL_cocoanotification.m */,
+ );
+ path = cocoa;
+ sourceTree = "<group>";
+ };
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@@ -3199,6 +3240,9 @@
0000A03C0F32C43816F40000 /* SDL_asyncio_windows_ioring.c in Sources */,
0000A877C7DB9FA935FC0000 /* SDL_uikitpen.m in Sources */,
63124A422E5C357500A53610 /* SDL_hidapi_zuiki.c in Sources */,
+ 00009183ED11C92F23FC0000 /* SDL_notification.c in Sources */,
+ 0000FB5C9B8CE5929A250000 /* SDL_cocoanotification.m in Sources */,
+ 0000AF1D2CED20010C2D0000 /* SDL_notificationevents.c in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/src/SDL.c b/src/SDL.c
index ab814bd318169..d3b3b09c934da 100644
--- a/src/SDL.c
+++ b/src/SDL.c
@@ -359,6 +359,11 @@ bool SDL_InitSubSystem(SDL_InitFlags flags)
SDL_DBus_Init();
#endif
+#ifdef SDL_PLATFORM_APPLE
+ // Apple platforms require the notification delegate to be registered early.
+ Cocoa_RegisterNotificationDelegate();
+#endif
+
#ifdef SDL_PLATFORM_WINDOWS
if (flags & (SDL_INIT_HAPTIC | SDL_INIT_JOYSTICK)) {
if (!SDL_HelperWindowCreate()) {
diff --git a/src/notification/SDL_notification_c.h b/src/notification/SDL_notification_c.h
index d4b72aa348485..0bdf367ad644f 100644
--- a/src/notification/SDL_notification_c.h
+++ b/src/notification/SDL_notification_c.h
@@ -27,6 +27,10 @@
extern SDL_NotificationID SDL_SYS_ShowNotification(SDL_PropertiesID props);
extern void SDL_CleanupNotifications();
+#ifdef SDL_PLATFORM_APPLE
+extern void Cocoa_RegisterNotificationDelegate();
+#endif
+
#ifdef SDL_VIDEO_DRIVER_WAYLAND
extern const char *SDL_GetNotificationActivationToken();
#endif
diff --git a/src/notification/cocoa/SDL_cocoanotification.m b/src/notification/cocoa/SDL_cocoanotification.m
new file mode 100644
index 0000000000000..c826e9875c934
--- /dev/null
+++ b/src/notification/cocoa/SDL_cocoanotification.m
@@ -0,0 +1,360 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+#include "SDL_internal.h"
+
+#include "../SDL_notification_c.h"
+
+// tvOS doesn't support the notification features SDL cares about.
+#ifndef SDL_PLATFORM_TVOS
+
+#include "../../events/SDL_notificationevents_c.h"
+#include "../../video/SDL_surface_c.h"
+
+#import <Foundation/Foundation.h>
+#import <UserNotifications/UNNotification.h>
+#import <UserNotifications/UNNotificationAction.h>
+#import <UserNotifications/UNNotificationAttachment.h>
+#import <UserNotifications/UNNotificationCategory.h>
+#import <UserNotifications/UNNotificationContent.h>
+#import <UserNotifications/UNNotificationRequest.h>
+#import <UserNotifications/UNNotificationResponse.h>
+#import <UserNotifications/UNNotificationSettings.h>
+#import <UserNotifications/UNNotificationSound.h>
+#import <UserNotifications/UNUserNotificationCenter.h>
+
+@interface SDLNotificationDelegate : NSObject <UNUserNotificationCenterDelegate>
+@end
+
+@implementation SDLNotificationDelegate
+- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
+ API_AVAILABLE(macos(10.14))
+{
+ if (@available(macOS 11, iOS 14, *)) {
+ completionHandler(UNNotificationPresentationOptionBanner + UNNotificationPresentationOptionSound);
+ } else {
+ completionHandler(UNNotificationPresentationOptionAlert + UNNotificationPresentationOptionSound);
+ }
+}
+
+- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler
+ API_AVAILABLE(macos(10.14))
+{
+ NSString *SDL_Identifier = @"SDL_LocalNotification-";
+ NSString *identifier = [[[response notification] request] identifier];
+ // const char *identifier = [[[[response notification] request] identifier] UTF8String];
+ SDL_NotificationID id = 0;
+
+ if ([identifier compare:SDL_Identifier options:0 range:NSMakeRange(0, [SDL_Identifier length])] == 0) {
+ id = (SDL_NotificationID)[[identifier substringFromIndex:[SDL_Identifier length]] integerValue];
+ }
+
+ if (id) {
+ NSString *action_id = [response actionIdentifier];
+ if (action_id) {
+ if ([action_id isEqualToString:UNNotificationDefaultActionIdentifier]) {
+ SDL_SendNotificationAction(id, "default");
+ } else {
+ if ([action_id length] != 0) {
+ SDL_SendNotificationAction(id, [action_id UTF8String]);
+ }
+ }
+ }
+ }
+
+ completionHandler();
+}
+@end
+
+API_AVAILABLE(macos(10.14))
+static UNUserNotificationCenter *center;
+static SDLNotificationDelegate *delegate;
+
+static bool ShouldEnableNotifications()
+{
+#if defined(SDL_PLATFORM_MACOS)
+ /* Notifications outside of an app bundle are unsupported, and will crash with an
+ * unhandled exception error, deep within a system library.
+ *
+ * FIXME: These functions are deprecated, find a modern way.
+ */
+ CFBundleRef bundle = CFBundleGetMainBundle();
+ CFURLRef bundleUrl = CFBundleCopyBundleURL(bundle);
+
+ CFStringRef uti;
+ if (CFURLCopyResourcePropertyForKey(bundleUrl, kCFURLTypeIdentifierKey, &uti, NULL) &&
+ uti && UTTypeConformsTo(uti, kUTTypeApplicationBundle)) {
+ return true;
+ }
+
+ return false;
+#else
+ // iOS can always enable notificaions.
+ return true;
+#endif
+}
+
+static NSURL *SaveTempImage(SDL_Surface *image)
+{
+ @autoreleasepool {
+ const UInt32 hash = SDL_murmur3_32(image->pixels, image->pitch * image->h, 0);
+ NSString *tempFileName = [NSString stringWithFormat:@"SDL_tmpimage-%u.png", hash];
+ NSString *tempFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:tempFileName];
+
+ if (!SDL_SavePNG(image, [tempFilePath fileSystemRepresentation])) {
+ return nil;
+ }
+
+ return [NSURL fileURLWithPath:tempFilePath];
+ }
+}
+
+void Cocoa_RegisterNotificationDelegate()
+{
+ if (!ShouldEnableNotifications()) {
+ return;
+ }
+
+ if (@available(macOS 10.14, *)) {
+ @autoreleasepool {
+ if (!center) {
+ center = [UNUserNotificationCenter currentNotificationCenter];
+ }
+ if (!delegate) {
+ delegate = [SDLNotificationDelegate new];
+ [center setDelegate:delegate];
+ }
+ }
+ }
+}
+
+bool SDL_RequestNotificationPermission(void)
+{
+ @autoreleasepool {
+ if (@available(macOS 10.14, *)) {
+ // Notifications not initialized (not in a bundle).
+ if (!center) {
+ return SDL_SetError("macOS notifications not supported outside an application bundle");
+ }
+
+ // Check authorization to send notifications, and request it if necessary.
+ [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings *_Nonnull settings) {
+ if (settings.authorizationStatus == UNAuthorizationStatusNotDetermined) {
+ UNAuthorizationOptions options = UNAuthorizationOptionAlert + UNAuthorizationOptionSound;
+ [center requestAuthorizationWithOptions:options
+ completionHandler:^(BOOL granted, NSError *_Nullable error) {}];
+ }
+ }];
+
+ return true;
+ } else {
+ SDL_SetError("Notifications require macOS 10.14 or higher");
+ return false;
+ }
+ }
+
+ return false;
+}
+
+bool SDL_RemoveNotification(SDL_NotificationID notification)
+{
+ @autoreleasepool {
+ if (@available(macOS 10.14, *)) {
+ // Notifications not initialized (not in a bundle).
+ if (!center) {
+ return SDL_SetError("macOS notifications not supported outside an application bundle");
+ }
+
+ NSString *identifier = [NSString stringWithFormat:@"SDL_LocalNotification-%u", notification];
+ [center removePendingNotificationRequestsWithIdentifiers:@[ identifier ]];
+ [center removeDeliveredNotificationsWithIdentifiers:@[ identifier ]];
+ }
+ }
+
+ return true;
+}
+
+SDL_NotificationID SDL_SYS_ShowNotification(SDL_PropertiesID props)
+{
+ @autoreleasepool {
+ if (@available(macOS 10.14, *)) {
+ if (!SDL_RequestNotificationPermission()) {
+ return false;
+ }
+
+ // Get the notification properties.
+ const char *title = SDL_GetStringProperty(props, SDL_PROP_NOTIFICATION_TITLE_STRING, NULL);
+ const char *message = SDL_GetStringProperty(props, SDL_PROP_NOTIFICATION_MESSAGE_STRING, "");
+ const char *sound = SDL_GetStringProperty(props, SDL_PROP_NOTIFICATION_SOUND_STRING, "default");
+ SDL_Surface *image = SDL_GetPointerProperty(props, SDL_PROP_NOTIFICATION_IMAGE_POINTER, NULL);
+ const SDL_NotificationID replaces = (SDL_NotificationID)SDL_GetNumberProperty(props, SDL_PROP_NOTIFICATION_REPLACES_NUMBER, 0);
+ const SDL_NotificationPriority priority = (SDL_NotificationPriority)SDL_GetNumberProperty(props, SDL_PROP_NOTIFICATION_PRIORITY_NUMBER, SDL_NOTIFICATION_PRIORITY_NORMAL);
+ const SDL_NotificationAction *sdlactions = SDL_GetPointerProperty(props, SDL_PROP_NOTIFICATION_ACTIONS_POINTER, NULL);
+ const int num_sdlactions = (int)SDL_GetNumberProperty(props, SDL_PROP_NOTIFICATION_ACTION_COUNT_NUMBER, 0);
+ const bool transient = SDL_GetBooleanProperty(props, SDL_PROP_NOTIFICATION_TRANSIENT_BOOLEAN, false);
+
+ // Generate a new ID.
+ Uint32 new_id;
+ if (replaces) {
+ new_id = replaces;
+ } else if (SecRandomCopyBytes(kSecRandomDefault, sizeof(new_id), &new_id) != errSecSuccess) {
+ new_id = (Uint32)SDL_GetTicksNS();
+ }
+
+ // Build the action array.
+ NSMutableArray *actions = nil;
+ if (sdlactions && num_sdlactions) {
+ actions = [NSMutableArray array];
+ for (int i = 0; i < num_sdlactions; ++i) {
+ if (sdlactions[i].type == SDL_NOTIFICATION_ACTION_TYPE_BUTTON) {
+ UNNotificationAction *action = [UNNotificationAction actionWithIdentifier:[NSString stringWithUTF8String:sdlactions[i].button.action_id]
+ title:[NSString stringWithUTF8String:sdlactions[i].button.action_label]
+ options:UNNotificationActionOptionNone];
+ actions[i] = action;
+ }
+ }
+ }
+
+ // Create the category.
+ NSString *category_id = nil;
+ if (actions && [actions count]) {
+ // Create the notification category.
+ category_id = [[NSUUID new] UUIDString];
+ UNNotificationCategory *category = [UNNotificationCategory categoryWithIdentifier:category_id
+ actions:actions
+ intentIdentifiers:@[]
+ options:UNNotificationCategoryOptionNone];
+ NSSet *categories = [NSSet setWithObject:category];
+ [center setNotificationCategories:categories];
+ }
+
+ // Configure the content.
+ UNMutableNotificationContent *content = [UNMutableNotificationContent new];
+ content.title = [NSString stringWithUTF8String:title];
+ content.body = [NSString stringWithUTF8String:message];
+ content.categoryIdentifier = category_id;
+
+ if (SDL_strcmp(sound, "default") == 0) {
+ if (priority == SDL_NOTIFICATION_PRIORITY_CRITICAL) {
+ // defaultCriticalSound is only in iOS 12+
+ if (@available(iOS 12, *)) {
+ content.sound = [UNNotificationSound defaultCriticalSound];
+ } else {
+ content.sound = [UNNotificationSound defaultSound];
+ }
+ } else {
+ content.sound = [UNNotificationSound defaultSound];
+ }
+ } else if (SDL_strcmp(sound, "silent") != 0) {
+ if (priority == SDL_NOTIFICATION_PRIORITY_CRITICAL) {
+ if (@available(iOS 12, *)) {
+ content.sound = [UNNotificationSound criticalSoundNamed:[NSString stringWithUTF8String:sound]];
+ } else {
+ content.sound = [UNNotificationSound soundNamed:[NSString stringWithUTF8String:sound]];
+ }
+ } else {
+ content.sound = [UNNotificationSound soundNamed:[NSString stringWithUTF8String:sound]];
+ }
+ }
+
+ if (@available(macOS 12, iOS 15, *)) {
+ switch (priority) {
+ case SDL_NOTIFICATION_PRIORITY_LOW:
+ content.interruptionLevel = UNNotificationInterruptionLevelPassive;
+ break;
+ case SDL_NOTIFICATION_PRIORITY_CRITICAL:
+ content.interruptionLevel = UNNotificationInterruptionLevelCritical;
+ break;
+ case SDL_NOTIFICATION_PRIORITY_NORMAL:
+ case SDL_NOTIFICATION_PRIORITY_HIGH:
+ default:
+ content.interruptionLevel = UNNotificationInterruptionLevelActive;
+ break;
+ }
+ }
+
+ // Notifications load images from file paths, so save it to a temporary location.
+ if (image) {
+ NSURL *url = SaveTempImage(image);
+ if (url) {
+ UNNotificationAttachment *attach = [UNNotificationAttachment attachmentWithIdentifier:@"" URL:url options:nil error:nil];
+ content.attachments = @[ attach ];
+ }
+ }
+
+ NSString *identifier = [NSString stringWithFormat:@"SDL_LocalNotification-%u", new_id];
+ UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier
+ content:content
+ trigger:nil];
+
+ [center addNotificationRequest:request
+ withCompletionHandler:^(NSError *_Nullable error) {
+ if (error != nil) {
+ SDL_SetError("Failed to show notification");
+ }
+ }];
+
+ /* There is no way to tell if the notification was ignored or wound up
+ * in the notification center, so just remove transient notifications
+ * after a brief period via a timer.
+ */
+ if (transient) {
+ [NSTimer scheduledTimerWithTimeInterval:7 repeats:NO block:^(NSTimer *timer) {
+ SDL_RemoveNotification(new_id);
+ }];
+ }
+
+ return new_id;
+ } else {
+ SDL_SetError("macOS 10.14+ required for notifications");
+ }
+
+ return 0;
+ }
+}
+#else
+
+// Notifications on tvOS are just for updating badges, and are of no use here.
+SDL_NotificationID SDL_SYS_ShowNotification(SDL_PropertiesID props)
+{
+ SDL_SetError("Notifications not supported on tvOS");
+ return 0;
+}
+
+void Cocoa_RegisterNotificationDelegate()
+{
+}
+
+bool SDL_RequestNotificationPermission(void)
+{
+ return SDL_SetError("Notifications not supported on tvOS");
+}
+
+bool SDL_RemoveNotification(SDL_NotificationID notification)
+{
+ return SDL_SetError("Notifications not supported on tvOS");
+}
+#endif
+
+void SDL_CleanupNotifications()
+{
+ // TODO: Anything to do here?
+}
diff --git a/src/notification/dummy/SDL_dummynotification.c b/src/notification/dummy/SDL_dummynotification.c
index 973207914b997..fc2cb8ac06ca3 100644
--- a/src/notification/dummy/SDL_dummynotification.c
+++ b/src/notification/dummy/SDL_dummynotification.c
@@ -43,6 +43,13 @@ void SDL_CleanupNotifications()
// Nothing to do.
}
+#ifdef SDL_PLATFORM_APPLE
+void Cocoa_RegisterNotificationDelegate()
+{
+ // Nothing to do.
+}
+#endif
+
#ifdef SDL_VIDEO_DRIVER_WAYLAND
const char *SDL_GetNotificationActivationToken()
{