Xamarin.iOS 中的动态通知操作按钮

在 iOS 12 中,通知可以动态添加、移除和更新其关联的操作按钮。 这种自定义使得能够为用户提供与通知内容和用户与它的交互直接相关的操作。

示例应用:RedGreenNotifications

本指南中的代码片段来自示例应用,该应用演示了如何使用 Xamarin.iOS 处理 iOS 12 中的通知操作按钮。

此示例应用发送了两种类型的本地通知:红色和绿色。 让应用发送通知后,使用 3D Touch 查看其自定义用户界面。 然后,使用通知的操作按钮旋转它显示的图像。 当图像旋转时,“重置旋转”按钮会根据需要显示并消失。

本指南中的代码片段来自此示例应用。

默认操作按钮

通知的类别决定了它的默认操作按钮。

在应用程序启动时创建和注册通知类别。 例如,在示例应用中,AppDelegateFinishedLaunching 方法执行以下操作:

  • 为红色通知定义一个类别,为绿色通知定义另一个类别
  • 通过调用以下项来注册这些类别:UNUserNotificationCenterSetNotificationCategories 方法
  • 附加单个 UNNotificationAction 到每个类别

以下示例代码演示了其工作原理:

public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
{
    // Request authorization to send notifications
    UNUserNotificationCenter center = UNUserNotificationCenter.Current;
    var options = UNAuthorizationOptions.Alert | UNAuthorizationOptions.Sound | UNAuthorizationOptions.Provisional | UNAuthorizationOptions.ProvidesAppNotificationSettings;
    center.RequestAuthorization(options, (bool success, NSError error) =>
    {
        // ...
        var rotateTwentyDegreesAction = UNNotificationAction.FromIdentifier("rotate-twenty-degrees-action", "Rotate 20°", UNNotificationActionOptions.None);

        var redCategory = UNNotificationCategory.FromIdentifier(
            "red-category",
            new UNNotificationAction[] { rotateTwentyDegreesAction },
            new string[] { },
            UNNotificationCategoryOptions.CustomDismissAction
        );

        var greenCategory = UNNotificationCategory.FromIdentifier(
            "green-category",
            new UNNotificationAction[] { rotateTwentyDegreesAction },
            new string[] { },
            UNNotificationCategoryOptions.CustomDismissAction
        );

        var set = new NSSet<UNNotificationCategory>(redCategory, greenCategory);
        center.SetNotificationCategories(set);
    });
    // ...
}

基于此代码,任何通知中 Content.CategoryIdentifier 为“red-category”或“green-category”的话,它会默认显示“旋转 20°”操作按钮。

通知操作按钮的应用内处理

UNUserNotificationCenter 具有类型为 IUNUserNotificationCenterDelegateDelegate 属性。

在示例应用中,AppDelegateFinishedLaunching 中将自身设置为用户通知中心的委托:

public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
{
    // Request authorization to send notifications
    UNUserNotificationCenter center = UNUserNotificationCenter.Current;
    var options = // ...
    center.RequestAuthorization(options, (bool success, NSError error) =>
    {
        center.Delegate = this;
        // ...

然后,AppDelegate 会实现 DidReceiveNotificationResponse 来处理操作按钮点击:

[Export("userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:")]
public void DidReceiveNotificationResponse(UNUserNotificationCenter center, UNNotificationResponse response, System.Action completionHandler)
{
    if (response.IsDefaultAction)
    {
        Console.WriteLine("ACTION: Default");
    }
    if (response.IsDismissAction)
    {
        Console.WriteLine("ACTION: Dismiss");
    }
    else
    {
        Console.WriteLine($"ACTION: {response.ActionIdentifier}");
    }

    completionHandler();
        }

DidReceiveNotificationResponse 的这个实现不会处理通知的“旋转 20°”操作按钮。 相反,通知的内容扩展会处理此按钮上的点击。 下一部分进一步讨论了通知操作按钮的处理。

通知内容扩展中的操作按钮

通知内容扩展包含一个视图控制器,用于定义通知的自定义接口。

此视图控制器可以使用 GetNotificationActionsSetNotificationActions 方法,面向其 ExtensionContext 属性,用于访问和修改通知的操作按钮。

在示例应用中,通知内容扩展的视图控制器仅在响应对已存在的操作按钮的点击时修改操作按钮。

注意

通知内容扩展可以响应视图控制器的 DidReceiveNotificationResponse 方法中的操作按钮点击,它被声明为 IUNNotificationContentExtension 的一部分。

尽管它与上述DidReceiveNotificationResponse 方法有着相同的名称,但这是一种不同的方法。

通知内容扩展完成对按钮点击的处理后,它可以选择是否让主应用程序处理同一按钮点击。 为此,它必须将 UNNotificationContentExtensionResponseOption 的相应值传递给其完成事件处理器:

  • Dismiss 指示应关闭通知界面,并且主应用不需要处理按钮点击。
  • DismissAndForwardAction 指示应关闭通知界面,并且主应用还应处理按钮点击。
  • DoNotDismiss 指示不应关闭通知界面,并且主应用不需要处理按钮点击。

内容扩展的 DidReceiveNotificationResponse 方法确定点击了哪个操作按钮,旋转通知界面中的图像,并显示或隐藏重置操作按钮:

[Export("didReceiveNotificationResponse:completionHandler:")]
public void DidReceiveNotificationResponse(UNNotificationResponse response, Action<UNNotificationContentExtensionResponseOption> completionHandler)
{
    var rotationAction = ExtensionContext.GetNotificationActions()[0];

    if (response.ActionIdentifier == "rotate-twenty-degrees-action")
    {
        rotationButtonTaps += 1;

        double radians = (20 * rotationButtonTaps) * (2 * Math.PI / 360.0);
        Xamagon.Transform = CGAffineTransform.MakeRotation((float)radians);

        // 9 rotations * 20 degrees = 180 degrees. No reason to
        // show the reset rotation button when the image is half
        // or fully rotated.
        if (rotationButtonTaps % 9 == 0)
        {
            ExtensionContext.SetNotificationActions(new UNNotificationAction[] { rotationAction });
        }
        else if (rotationButtonTaps % 9 == 1)
        {
            var resetRotationAction = UNNotificationAction.FromIdentifier("reset-rotation-action", "Reset rotation", UNNotificationActionOptions.None);
            ExtensionContext.SetNotificationActions(new UNNotificationAction[] { rotationAction, resetRotationAction });
        }
    }

    if (response.ActionIdentifier == "reset-rotation-action")
    {
        rotationButtonTaps = 0;

        double radians = (20 * rotationButtonTaps) * (2 * Math.PI / 360.0);
        Xamagon.Transform = CGAffineTransform.MakeRotation((float)radians);

        ExtensionContext.SetNotificationActions(new UNNotificationAction[] { rotationAction });
    }

    completionHandler(UNNotificationContentExtensionResponseOption.DoNotDismiss);
}

在这种情况下,该方法会将 UNNotificationContentExtensionResponseOption.DoNotDismiss 传递给其完成事件处理器。 这意味着通知的界面将保持打开状态。