For my app, I don't have a finite set of buttons that need to be supported. Instead, we are sending the buttons within the notification payload. Because of this, I need to be able to create the UNNotificationCategory on the fly, each time a notification is received with the buttons within the payload.
In order to accomplish this, I create a Notification Service Extension to intercept the notification, parse the payload, create the categories, and update the category of the push notification. That all seems doable but in practice, when creating the UNNotificationCategory from within the Notification Service Extension, the push notification does not have the actions for that cateogory.
Testing: I've put a block of code in FinishedLaunching to print out all the UNNotificationCategories and it always returns empty. I'm not sure if this is reasonable info, though, since I'm not sure if FinishedLaunching runs before the Notification Externsion. I had a feeling that the UNUserNotificationCenter.Current instance was different between projects but I created a test library project where I created Categories and they were available back within the main project.
Other Notes: When creating test UNNotificationCategories within FinishedLaunching and then only updating the categoryID of the push notification in the Notification Extension it works. But this isn't dynamic.
Why aren't the categories I create in the Notification Service Extension available to the push notification?
Code:
AppDelegate.cs (Main Project)
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);
}
I finally found an answer, though it doesn't seem like the correct one.
Stumbling across https://learn.microsoft.com/en-us/xamarin/ios/platform/introduction-to-ios12/notifications/dynamic-actions, the "Action buttons in the notification content extension" section briefly mentions using the ViewController's
ExtensionContext
to call SetNotificationActions()
. This does work.
Full Solution:
public void DidReceiveNotification(UNNotification notification)
{
var content = notification.Request.Content;
var actions = new List<UNNotificationAction>();
List<SimplePushNotificationButton> buttons = new List<SimplePushNotificationButton>();
if (content.UserInfo.TryGetValue(new NSString("buttonJSON"), out NSObject buttonsObject))
{
string buttonsString = buttonsObject.ToString();
if (!string.IsNullOrEmpty(buttonsString))
{
buttons = JsonConvert.DeserializeObject<List<SimplePushNotificationButton>>(buttonsString);
for (int i = 0; i < buttons.Count; i++)
{
// use the button text within the action id so we can match them later in AppDelegate.HandlePushNotificationButton
// TODO: add icon?
var action = UNNotificationAction.FromIdentifier(
$"{Guid.NewGuid()}_{buttons[i].Text}",
buttons[i].Text, buttons[i].Action.URLAction == URLAction.Post ? UNNotificationActionOptions.None : UNNotificationActionOptions.Foreground);
actions.Add(action);
}
}
}
// This is what actually adds the buttons
ExtensionContext.SetNotificationActions(actions.ToArray());
}
[Export("didReceiveNotificationResponse:completionHandler:")]
public void DidReceiveNotification(UNNotificationResponse response, Action<UNNotificationContentExtensionResponseOption> completionHandler)
{
// We want to handle the button tap in the main project, not this one
completionHandler(UNNotificationContentExtensionResponseOption.DismissAndForwardAction);
}
I don't think this is the best solution as no where else is it recommended to use the Extension Context to add buttons. Everywhere else, even Apple's official documentation, says to create a Category with the Actions and then make sure your notification has a matching CategoryID.
The only slight downside of this solution is the buttons take about a second to show up after 3D touching the notification due to DidReceiveNotification
not running until then.