Explorar el Código

Add the Cocoa notification driver

Supported on macOS 10.14+ and iOS.
Frank Praznik hace 2 meses
padre
commit
ba7c0b897b

+ 44 - 0
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;
 		};

+ 5 - 0
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()) {

+ 4 - 0
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

+ 360 - 0
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?
+}

+ 7 - 0
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()
 {