Xamarin:Notification Service Extension创建UNNotificationCategory无效问题
我的应用不需要固定的操作按钮集合,而是要通过通知负载动态传递按钮信息,所以需要在每次收到带按钮的通知时,动态创建UNNotificationCategory。
为实现这个需求,我创建了Notification Service Extension来拦截通知、解析负载、创建类别并更新推送通知的类别ID。理论上这个方案可行,但实际测试发现,在Notification Service Extension中创建UNNotificationCategory后,推送通知并没有显示对应的操作按钮。
测试情况
- 在主项目的
FinishedLaunching方法中添加了打印所有UNNotificationCategory的代码,结果始终为空。 - 不确定
FinishedLaunching是否在Notification Extension之前执行。 - 曾怀疑
UNUserNotificationCenter.Current实例在主项目和扩展中不一致,但测试发现库项目中创建的类别可以在主项目中访问。
其他说明
如果在FinishedLaunching中预先创建测试用的UNNotificationCategory,仅在Notification Extension中修改推送通知的categoryID,功能可以正常工作,但这无法实现动态创建的需求。
请问:为什么在Notification Service Extension中创建的类别无法被推送通知识别?
AppDelegate.cs(主项目)
public override bool FinishedLaunching(UIApplication app, NSDictionary options) { Rg.Plugins.Popup.Popup.Init(); global::Xamarin.Forms.Forms.Init(); IOSMigrationHelper migrationHelper = new IOSMigrationHelper(); // must be done before Firebase.Core.App.Configure() to migrate the saved fcm token // and before LoadApplication() so we don't run the initial setup process // and after Xamarin.Forms.Forms.Init() so we can use DependencyService migrationHelper.Migrate(); Forms9Patch.iOS.Settings.Initialize(this); LoadApplication(new App()); bool success = base.FinishedLaunching(app, options); if (success) { AllGesturesRecognizer allGesturesRecognizer = new AllGesturesRecognizer(delegate { SessionManager.Instance.ExtendSession(); }); Window.AddGestureRecognizer(allGesturesRecognizer); } if (string.IsNullOrEmpty(AppPreferences.DeviceIdentifier)) { // this will change if the user removes and reinstalls the app but there isn't anything else to use AppPreferences.DeviceIdentifier = UIDevice.CurrentDevice.IdentifierForVendor.ToString(); } // Test Categories. Creating them here and then just updating the categoryID in the notification extension works var actions1 = new List<UNNotificationAction>(); var action11 = UNNotificationAction.FromIdentifier($"C1 B1", "C1 B1", UNNotificationActionOptions.None); // TODO: change to foregound if urlaction != post var action12 = UNNotificationAction.FromIdentifier($"C1 B2", "C1 B2", UNNotificationActionOptions.None); var actions2 = new List<UNNotificationAction>(); var action21 = UNNotificationAction.FromIdentifier($"C2 B1", "C2 B1", UNNotificationActionOptions.None); var action22 = UNNotificationAction.FromIdentifier($"C2 B2", "C2 B2", UNNotificationActionOptions.None); actions1.Add(action11); actions1.Add(action12); actions2.Add(action21); actions2.Add(action22); var intentIDs = new string[] { }; var category1 = UNNotificationCategory.FromIdentifier("cat1", actions1.ToArray(), intentIDs, UNNotificationCategoryOptions.AllowInCarPlay | UNNotificationCategoryOptions.AllowAnnouncement); var category2 = UNNotificationCategory.FromIdentifier("cat2", actions2.ToArray(), intentIDs, UNNotificationCategoryOptions.AllowInCarPlay | UNNotificationCategoryOptions.AllowAnnouncement); // Register category var categories = new UNNotificationCategory[] { category1, category2 }; UserNotificationCenter.Current.SetNotificationCategories(new NSSet<UNNotificationCategory>(categories)); PrintCategories(); SetupPushNotifications(app); return success; } async void PrintCategories() { try { NSSet<UNNotificationCategory> categories = await iOSNotificationCenter.Current.GetNotificationCategoriesAsync(); // Categories is null when the categories created in the AppDelegate are commented out and moved to the Notification Extension if (categories == null) { App.Current.MainPage.DisplayAlert($"No categories", "", "Ok"); return; } foreach (UNNotificationCategory c in categories) { App.Current.MainPage.DisplayAlert($"Category: {c.Identifier}", "", "Ok"); } } catch (Exception e) { App.Current.MainPage.DisplayAlert(e.ToString(), "", "Ok"); } }
NotificationService.cs(Notification Service Extension)
public override void DidReceiveNotificationRequest(UNNotificationRequest request, Action<UNNotificationContent> contentHandler) { ContentHandler = contentHandler; BestAttemptContent = (UNMutableNotificationContent)request.Content.MutableCopy(); var categoryID = "but1"; // creating the categories in the notification service extension doesn't work. The notification doesn't have any buttons var actions = new List<UNNotificationAction>(); var action1 = UNNotificationAction.FromIdentifier($"Button 1", "Button 1", UNNotificationActionOptions.None); var action2 = UNNotificationAction.FromIdentifier($"Button 2", "Button 2", UNNotificationActionOptions.None); actions.Add(action1); actions.Add(action2); var intentIDs = new string[] { }; var category = UNNotificationCategory.FromIdentifier(categoryID, actions.ToArray(), intentIDs, UNNotificationCategoryOptions.AllowInCarPlay | UNNotificationCategoryOptions.AllowAnnouncement); //// Register category var categories = new UNNotificationCategory[] { category }; UNUserNotificationCenter.Current.SetNotificationCategories(new NSSet<UNNotificationCategory>(categories)); BestAttemptContent.CategoryIdentifier = categoryID; ContentHandler(BestAttemptContent); }
核心原因
iOS系统在处理推送通知并展示操作按钮的流程中,会在触发Notification Service Extension之前就检查UNNotificationCategory的注册状态。也就是说,当你的Extension开始创建类别时,系统已经完成了对通知类别有效性的校验,此时新注册的类别无法被当前通知复用。
另外,Notification Service Extension和主App是两个独立的进程,虽然它们共享同一个UNUserNotificationCenter的底层存储,但系统在处理当前通知时,不会等待Extension中的类别注册完成再去加载按钮配置。
解决方案
提前注册所有可能的类别模板:如果按钮的类型、数量有固定的几种组合,可以在主App启动时(
FinishedLaunching)提前注册所有对应的UNNotificationCategory,通知负载中只传递类别ID即可。这也是你测试中能正常工作的方案。使用通知服务扩展配合本地通知转发:
- 在Notification Service Extension中拦截远程通知,解析出按钮信息后,先取消原远程通知的展示。
- 动态创建
UNNotificationCategory并注册,然后创建一个本地通知,设置对应的类别ID,最后展示这个本地通知。
注意:这种方式需要处理好通知的展示时机和权限,同时要避免重复通知。
使用iOS 14+的交互式通知新API:如果你的应用只需要支持iOS 14及以上版本,可以考虑利用通知内容扩展(Notification Content Extension)来自定义通知界面,直接在扩展中渲染按钮并处理点击事件,无需依赖
UNNotificationCategory。
额外说明
你在FinishedLaunching中打印不到Extension注册的类别,是因为主App进程和Extension进程是分开启动的。当Extension运行时,主App可能处于未启动状态,此时Extension注册的类别会被存储,但主App启动时如果没有重新注册这些类别,调用GetNotificationCategoriesAsync可能无法获取到(因为主App进程的UNUserNotificationCenter实例需要重新加载存储的类别)。
内容的提问来源于stack exchange,提问作者Tyler Wanta

