Xamarin.iOS 中的增强用户通知

iOS 10 的新功能“用户通知”框架允许发送和处理本地和远程通知。 使用此框架,应用和应用扩展可以通过指定一组条件(例如位置或时刻)来计划本地通知的发送。

关于用户通知

如上所述,新的用户通知框架允许发送和处理本地和远程通知。 使用此框架,应用和应用扩展可以通过指定一组条件(例如位置或时刻)来计划本地通知的发送。

此外,应用或扩展可以在本地和远程通知传递给用户的 iOS 设备时接收(并可能修改)这些通知。

新的用户通知 UI 框架允许应用或应用扩展自定义本地和远程通知呈现给用户时的外观。

此框架提供了应用向用户发送通知的以下方式:

  • 视觉警报 - 通知作为横幅从屏幕顶部向下滚动。
  • 声音和振动 - 可与通知相关联。
  • 应用图标标记 - 应用的图标显示一个标记,表明有新内容可用,例如未读电子邮件的数量。

此外,根据用户的当前上下文,有不同的方式来显示通知:

  • 如果设备已解锁,则通知将作为横幅从屏幕顶部向下滚动。
  • 如果设备已锁定,则通知将显示在用户的锁屏界面上。
  • 如果用户错过了通知,他们可以打开通知中心并在那里查看任何可用的等待通知。

Xamarin.iOS 应用可以发送两种类型的用户通知:

  • 本地通知 - 这些由用户设备上本地安装的应用发送。
  • 远程通知 - 从远程服务器发送,向用户显示或者触发应用内容的后台更新。

关于本地通知

iOS 应用可以发送的本地通知具有以下功能和属性:

  • 它们由用户设备上的本地应用发送。
  • 可以将其配置为使用基于时间或位置的触发器。
  • 应用使用用户的设备安排通知,并在满足触发条件时显示通知。
  • 当用户与通知交互时,应用将收到回调。

本地通知的一些示例包括:

  • 日历警报
  • 提醒警报
  • 位置感知触发器

有关详细信息,请参阅 Apple 的本地和远程通知编程指南文档。

关于远程通知

iOS 应用可以发送的远程通知具有以下功能和属性:

  • 应用具有一个与之通信的服务器端组件。
  • Apple Push Notification 服务 (APNs) 用于尽最大努力将远程通知从开发人员的基于云的服务器传送到用户的设备。
  • 当应用收到远程通知时,它将显示给用户。
  • 当用户与通知交互时,应用将收到回调。

远程通知的一些示例包括:

  • 资讯警报
  • 体育更新
  • 即时消息传递消息

iOS 应用有两种类型的远程通知:

  • 面向用户 - 这些会显示给设备上的用户。
  • 无提示更新 - 这些提供一种机制来在后台更新 iOS 应用的内容。 收到无提示更新后,应用可以联系远程服务器以获取最新内容。

有关详细信息,请参阅 Apple 的本地和远程通知编程指南文档。

关于现有通知 API

在 iOS 10 之前,iOS 应用将使用 UIApplication 向系统注册通知,并计划应如何触发通知(按时间或位置)。

使用现有通知 API 时,开发人员可能会遇到以下几个问题:

  • 本地通知或远程通知需要不同的回调,这可能会导致代码重复。
  • 在系统安排好通知后,应用对通知的控制有限。
  • Apple 所有现有平台的支持程度不同。

关于新用户通知框架

借助 iOS 10,Apple 引入了新的用户通知框架,该框架取代了上面所述的现有 UIApplication 方法。

用户通知框架提供以下内容:

  • 一个熟悉的 API,它包括与以前方法相同的功能,可以轻松地从现有框架移植代码。
  • 包括一组扩展的内容选项,允许向用户发送更丰富的通知。
  • 本地通知和远程通知可以由相同的代码和回调处理。
  • 简化了处理用户与通知交互时发送到应用的回调的过程。
  • 增强了对挂起通知和已送达通知的管理,包括删除或更新通知的功能。
  • 添加了在应用内显示通知的功能。
  • 添加了从应用扩展内计划和处理通知的功能。
  • 为通知本身添加了新的扩展点。

新的用户通知框架跨 Apple 支持的多个平台提供统一通知 API,包括:

  • iOS - 全面支持管理和计划通知。
  • tvOS - 为本地和远程通知添加了标记应用图标的功能。
  • watchOS - 添加了将通知从用户的配对 iOS 设备转发到其 Apple Watch 的功能,并使手表应用能够直接在手表本身上执行本地通知。
  • macOS - 全面支持管理和计划通知。

有关详细信息,请参阅 Apple 的 UserNotifications 框架参考UserNotificationsUI 文档。

准备通知发送

在 iOS 应用可以向用户发送通知之前,该应用必须在系统中注册,并且由于通知会对用户造成中断,应用在发送通知之前必须显式请求权限。

用户可以对应用批准三种不同级别的通知请求:

  • 横幅显示。
  • 声音警报。
  • 标记应用图标。

此外,必须针对本地和远程通知请求和设置这些审批级别。

应在应用启动后立即请求通知权限,方法是将以下代码添加到 AppDelegateFinishedLaunching 方法并设置所需的通知类型(UNAuthorizationOptions):

注意

UNUserNotificationCenter 仅适用于 iOS 10+。 因此,在发送请求之前,最好先检查 macOS 版本。

using UserNotifications;
...

public override bool FinishedLaunching (UIApplication application, NSDictionary launchOptions)
{
    // Version check
    if (UIDevice.CurrentDevice.CheckSystemVersion (10, 0)) {
        // Request notification permissions from the user
        UNUserNotificationCenter.Current.RequestAuthorization (UNAuthorizationOptions.Alert, (approved, err) => {
            // Handle approval
        });
    }

    return true;
}

由于此 API 是统一的,并且也适用于 Mac 10.14+,如果面向 macOS,则还必须尽快检查通知权限:

using UserNotifications;
...

public override void DidFinishLaunching (NSNotification notification)
{
    // Check we're at least v10.14
    if (NSProcessInfo.ProcessInfo.IsOperatingSystemAtLeastVersion (new NSOperatingSystemVersion (10, 14, 0))) {
        // Request notification permissions from the user
        UNUserNotificationCenter.Current.RequestAuthorization (UNAuthorizationOptions.Alert | UNAuthorizationOptions.Badge | UNAuthorizationOptions.Sound, (approved, err) => {
            // Handle approval
        });
    }
}

> [!NOTE]
> With MacOS apps, for the permission dialog to appear, you must sign your macOS app, even if building locally in DEBUG mode. Therefore, **Project->Options->Mac Signing->Sign the application bundle** must be checked.

Additionally, a user can always change the notification privileges for an app at any time using the **Settings** app on the device. The app should check for the user's requested notification privileges before presenting a notification using the following code:

```csharp
// Get current notification settings
UNUserNotificationCenter.Current.GetNotificationSettings ((settings) => {
    var alertsAllowed = (settings.AlertSetting == UNNotificationSetting.Enabled);
});    

配置远程通知环境

对于 iOS 10,开发人员必须通知操作系统在开发还是生产环境中运行推送通知。 未能提供此信息可能会导致应用在提交到 iTune App Store 时被拒绝,通知如下所示:

缺少推送通知权利 - 应用包括 Apple Push Notification 服务的 API,但应用签名中缺少 aps-environment 权利。

若要提供所需的权利,请执行以下操作:

  1. Solution Pad 中双击 Entitlements.plist 文件,打开它进行编辑。

  2. 切换到“源”视图

    源视图

  3. 单击 + 按钮添加新键。

  4. 为“属性”输入 aps-environment,将“类型”保留为“String”,并为“”输入 developmentproduction

    aps-environment 属性

  5. 保存对文件所做的更改。

注册远程通知

如果应用将发送和接收远程通知,则仍需使用现有 UIApplication API 执行令牌注册。 此注册要求设备具有实时网络连接访问 APNs,这将生成发送到应用的必要令牌。 然后,应用需要将此令牌转发到开发人员的服务器端应用,以注册远程通知:

令牌注册概述

使用以下代码初始化所需的注册:

UIApplication.SharedApplication.RegisterForRemoteNotifications ();

发送到开发人员服务器端应用的令牌需要包含在发送远程通知时从服务器发送到 APNs 的通知有效负载的一部分:

作为通知有效负载的一部分的令牌

该令牌充当将通知与用于打开或响应通知的应用联系在一起的键。

有关详细信息,请参阅 Apple 的本地和远程通知编程指南文档。

通知传达

随着应用完全注册以及用户请求和授予的所需权限,该应用现在可以发送和接收通知。

提供通知内容

对于 iOS 10,所有通知都包含标题副标题,它们将始终与通知内容的正文一起显示。 此外,还新增了向通知内容添加媒体附件的功能。

若要创建本地通知的内容,请使用以下代码:

var content = new UNMutableNotificationContent();
content.Title = "Notification Title";
content.Subtitle = "Notification Subtitle";
content.Body = "This is the message body of the notification.";
content.Badge = 1;

对于远程通知,此过程类似:

{
    "aps":{
        "alert":{
            "title":"Notification Title",
            "subtitle":"Notification Subtitle",
            "body":"This is the message body of the notification."
        },
        "badge":1
    }
}

发送通知时计划

创建通知的内容后,应用需要通过设置触发器来安排何时向用户显示通知。 iOS 10 提供四种不同的触发器类型:

  • 推送通知 - 专用于远程通知,并在 APNs 将通知包发送到设备上运行的应用时触发。
  • 时间间隔 - 允许从现在开始到将来某个时间点结束,按某个时间间隔安排本地通知。 例如: var trigger = UNTimeIntervalNotificationTrigger.CreateTrigger (5, false);
  • 日历日期 - 允许为特定日期和时间安排本地通知。
  • 基于位置 - 允许在 iOS 设备进入或离开特定地理位置或者靠近任何蓝牙信标时安排本地通知。

当本地通知准备就绪时,应用需要调用 UNUserNotificationCenter 对象的 Add 方法来安排其向用户的显示。 对于远程通知,服务器端应用会将通知有效负载发送到 APNs,然后 APNs 将数据包发送到用户的设备。

将所有部分组合在一起,示例本地通知可能如下所示:

using UserNotifications;
...

var content = new UNMutableNotificationContent ();
content.Title = "Notification Title";
content.Subtitle = "Notification Subtitle";
content.Body = "This is the message body of the notification.";
content.Badge = 1;

var trigger =  UNTimeIntervalNotificationTrigger.CreateTrigger (5, false);

var requestID = "sampleRequest";
var request = UNNotificationRequest.FromIdentifier (requestID, content, trigger);

UNUserNotificationCenter.Current.AddNotificationRequest (request, (err) => {
    if (err != null) {
        // Do something with error...
    }
});

处理前台应用通知

在 iOS 10 中,当应用处于前台并触发通知时,它可以以不同方式处理通知。 通过提供 UNUserNotificationCenterDelegate 并实现 WillPresentNotification 方法,应用可以接管显示通知的责任。 例如:

using System;
using UserNotifications;

namespace MonkeyNotification
{
    public class UserNotificationCenterDelegate : UNUserNotificationCenterDelegate
    {
        #region Constructors
        public UserNotificationCenterDelegate ()
        {
        }
        #endregion

        #region Override Methods
        public override void WillPresentNotification (UNUserNotificationCenter center, UNNotification notification, Action<UNNotificationPresentationOptions> completionHandler)
        {
            // Do something with the notification
            Console.WriteLine ("Active Notification: {0}", notification);

            // Tell system to display the notification anyway or use
            // `None` to say we have handled the display locally.
            completionHandler (UNNotificationPresentationOptions.Alert);
        }
        #endregion
    }
}

此代码只是将 UNNotification 的内容写到应用程序输出,并要求系统显示通知的标准提醒。

如果应用希望在前台显示通知本身,而不使用系统默认值,请将 None 传递给完成事件处理器。 示例:

completionHandler (UNNotificationPresentationOptions.None);

完成此代码后,打开 AppDelegate.cs 文件进行编辑并更改 FinishedLaunching 方法,如下所示:

public override bool FinishedLaunching (UIApplication application, NSDictionary launchOptions)
{
    // Request notification permissions from the user
    UNUserNotificationCenter.Current.RequestAuthorization (UNAuthorizationOptions.Alert, (approved, err) => {
        // Handle approval
    });

    // Watch for notifications while the app is active
    UNUserNotificationCenter.Current.Delegate = new UserNotificationCenterDelegate ();

    return true;
}

此代码将自定义 UNUserNotificationCenterDelegate 从上面附加到当前 UNUserNotificationCenter,以便应用可以在处于活动状态并位于前台时处理通知。

通知管理

在 iOS 10 中,通知管理提供对挂起通知和已送达通知的访问权限,并增加了删除、更新或提升这些通知的功能。

通知管理的一个重要部分是请求标识符,该标识符是在系统创建和计划通知时分配给通知的。 对于远程通知,这是通过 HTTP 请求标头中的新 apps-collapse-id 字段分配的。

请求标识符用于选择应用希望对其执行通知管理的通知。

删除通知

若要从系统中删除挂起的通知,请使用以下代码:

var requests = new string [] { "sampleRequest" };
UNUserNotificationCenter.Current.RemovePendingNotificationRequests (requests);

若要删除已送达的通知,请使用以下代码:

var requests = new string [] { "sampleRequest" };
UNUserNotificationCenter.Current.RemoveDeliveredNotifications (requests);

更新现有通知

若要更新现有通知,只需创建一个修改了所需参数(如新的触发时间)的新通知,并将其添加到系统中,使其与需要修改的通知具有相同的请求标识符。 示例:

using UserNotifications;
...

// Rebuild notification
var content = new UNMutableNotificationContent ();
content.Title = "Notification Title";
content.Subtitle = "Notification Subtitle";
content.Body = "This is the message body of the notification.";
content.Badge = 1;

// New trigger time
var trigger = UNTimeIntervalNotificationTrigger.CreateTrigger (10, false);

// ID of Notification to be updated
var requestID = "sampleRequest";
var request = UNNotificationRequest.FromIdentifier (requestID, content, trigger);

// Add to system to modify existing Notification
UNUserNotificationCenter.Current.AddNotificationRequest (request, (err) => {
    if (err != null) {
        // Do something with error...
    }
});

对于已送达的通知,如果用户已经阅读了现有通知,则该通知将更新并提升到主页和锁屏界面上以及通知中心中列表的顶部。

使用通知操作

在 iOS 10 中,传递给用户的通知不是静态的,提供了用户与通知交互的多种方式(从内置到自定义操作)。

iOS 应用可以响应以下三种类型的操作:

  • 默认操作 - 这是当用户点击通知以打开应用并显示给定通知的详细信息时。
  • 自定义操作 - 这些是在 iOS 8 中添加的,为用户提供了一种直接从通知执行自定义任务而无需启动应用的快速方法。 它们可以显示为具有可自定义标题的按钮列表,也可以显示为文本输入字段,该字段可以在后台(给应用程序一小段时间来完成请求)或前台(在前台启动应用以完成请求)运行。 自定义操作在 iOS 和 watchOS 上都可用。
  • 消除操作 - 当用户取消给定的通知时,此操作将发送到应用。

创建自定义操作

若要在系统中创建和注册自定义操作,请使用以下代码:

// Create action
var actionID = "reply";
var title = "Reply";
var action = UNNotificationAction.FromIdentifier (actionID, title, UNNotificationActionOptions.None);

// Create category
var categoryID = "message";
var actions = new UNNotificationAction [] { action };
var intentIDs = new string [] { };
var categoryOptions = new UNNotificationCategoryOptions [] { };
var category = UNNotificationCategory.FromIdentifier (categoryID, actions, intentIDs, UNNotificationCategoryOptions.None);

// Register category
var categories = new UNNotificationCategory [] { category };
UNUserNotificationCenter.Current.SetNotificationCategories (new NSSet<UNNotificationCategory>(categories));

创建新的 UNNotificationAction 时,会为其分配唯一 ID 和按钮上显示的标题。 默认情况下,操作将创建为后台操作,但可以提供选项来调整操作的行为(例如将其设置为前台操作)。

创建的每个操作都需要与一个类别相关联。 创建新的 UNNotificationCategory 时,会为其分配唯一 ID、可执行的操作列表、意图 ID 列表(以提供有关类别中操作意图的详细信息),以及一些控制类别行为的选项。

最后,所有类别都使用 SetNotificationCategories 方法向系统注册。

显示自定义操作

创建了一组自定义操作和类别并在系统中注册后,可以从本地通知或远程通知显示它们。

对于远程通知,请在远程通知有效负载中设置与上面创建的类别之一匹配的 category。 例如:

{
    aps:{
        alert:"Hello world!",
        category:"message"
    }
}

对于本地通知,请设置 UNMutableNotificationContent 对象的 CategoryIdentifier 属性。 例如:

var content = new UNMutableNotificationContent ();
content.Title = "Notification Title";
content.Subtitle = "Notification Subtitle";
content.Body = "This is the message body of the notification.";
content.Badge = 1;
content.CategoryIdentifier = "message";
...

同样,此 ID 需要匹配上面创建的类别之一。

处理消除操作

如上所述,当用户关闭通知时,可以向应用发送消除操作。 由于这不是标准操作,因此在创建类别时需要设置一个选项。 例如:

var categoryID = "message";
var actions = new UNNotificationAction [] { action };
var intentIDs = new string [] { };
var categoryOptions = new UNNotificationCategoryOptions [] { };
var category = UNNotificationCategory.FromIdentifier (categoryID, actions, intentIDs, UNNotificationCategoryOptions.CustomDismissAction);

处理操作响应

当用户与上面创建的自定义操作和类别交互时,应用程序需要完成请求的任务。 这是通过提供 UNUserNotificationCenterDelegate 并实现 UserNotificationCenter 方法来完成的。 例如:

using System;
using UserNotifications;

namespace MonkeyNotification
{
    public class UserNotificationCenterDelegate : UNUserNotificationCenterDelegate
    {
        ...

        #region Override Methods
        public override void DidReceiveNotificationResponse (UNUserNotificationCenter center, UNNotificationResponse response, Action completionHandler)
        {
            // Take action based on Action ID
            switch (response.ActionIdentifier) {
            case "reply":
                // Do something
                break;
            default:
                // Take action based on identifier
                if (response.IsDefaultAction) {
                    // Handle default action...
                } else if (response.IsDismissAction) {
                    // Handle dismiss action
                }
                break;
            }

            // Inform caller it has been handled
            completionHandler();
        }
        #endregion
    }
}

传入的 UNNotificationResponse 类具有一个 ActionIdentifier 属性,该属性可以是默认操作或消除操作。 使用 response.Notification.Request.Identifier 测试任何自定义操作。

UserText 属性保存任何用户文本输入的值。 Notification 属性保存原始通知,其中包括带有触发器和通知内容的请求。 应用可以根据触发器类型确定它是本地通知还是远程通知。

注意

iOS 12 使自定义通知 UI 可以在运行时修改其操作按钮。 有关详细信息,请查看动态通知操作按钮文档。

使用服务扩展

使用远程通知时,服务扩展提供了在通知有效负载内启用端到端加密的方法。 服务扩展是在后台运行的非用户界面扩展(在 iOS 10 中提供),主要目的是在向用户显示通知之前扩充或替换通知的可见内容。

服务扩展概述

服务扩展旨在快速运行,并且只给系统很短的执行时间。 如果服务扩展未能在分配的时间内完成其任务,将调用回退方法。 如果回退失败,将向用户显示原始通知内容。

服务扩展的一些潜在使用包括:

  • 提供远程通知内容的端到端加密。
  • 将附件添加到远程通知以扩充它们。

实现服务扩展

若要在 Xamarin.iOS 应用中实现服务扩展,请执行以下操作:

  1. 在 Visual Studio for Mac 中打开应用的解决方案。

  2. 右键单击 Solution Pad 中的“解决方案名称”,然后选择“添加”>“添加新项目”。

  3. 选择“iOS”>“扩展”>“通知服务扩展”,然后单击“下一步”按钮:

    选择“通知服务扩展”

  4. 输入扩展的名称,然后单击“下一步”按钮

    输入扩展的名称

  5. 根据需要调整“项目名称”和/或“解决方案名称”,然后单击“创建”按钮

    调整项目名称和/或解决方案名称

重要

服务扩展的捆绑包标识符应与主应用的捆绑包标识符匹配,并在末尾附加 .appnameserviceextension。 例如,如果主应用的捆绑包标识符为 com.xamarin.monkeynotify,则服务扩展的捆绑包标识符应为 com.xamarin.monkeynotify.monkeynotifyserviceextension。 当扩展添加到解决方案时,应自动设置此项。

通知服务扩展中有一个主类需要修改才能提供所需的功能。 例如:

using System;
using Foundation;
using UIKit;
using UserNotifications;

namespace MonkeyChatServiceExtension
{
    [Register ("NotificationService")]
    public class NotificationService : UNNotificationServiceExtension
    {
        #region Computed Properties
        public Action<UNNotificationContent> ContentHandler { get; set; }
        public UNMutableNotificationContent BestAttemptContent { get; set; }
        #endregion

        #region Constructors
        protected NotificationService (IntPtr handle) : base (handle)
        {
            // Note: this .ctor should not contain any initialization logic.
        }
        #endregion

        #region Override Methods
        public override void DidReceiveNotificationRequest (UNNotificationRequest request, Action<UNNotificationContent> contentHandler)
        {
            ContentHandler = contentHandler;
            BestAttemptContent = (UNMutableNotificationContent)request.Content.MutableCopy ();

            // Modify the notification content here...
            BestAttemptContent.Title = $"{BestAttemptContent.Title}[modified]";

            ContentHandler (BestAttemptContent);
        }

        public override void TimeWillExpire ()
        {
            // Called just before the extension will be terminated by the system.
            // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.

            ContentHandler (BestAttemptContent);
        }
        #endregion
    }
}

第一个方法 DidReceiveNotificationRequest 将通过 request 对象传递通知标识符和通知内容。 需要调用传入的 contentHandler 来向用户显示通知。

第二个方法 TimeWillExpire 将在服务扩展处理请求的时间即将用完之前调用。 如果服务扩展未能在分配的时间内调用 contentHandler,则会向用户显示原始内容。

触发服务扩展

使用应用创建和交付服务扩展后,可以通过修改发送到设备的远程通知有效负载来触发它。 例如:

{
    aps : {
        alert : "New Message Available",
        mutable-content: 1
    },
    encrypted-content : "#theencryptedcontent"
}

新的 mutable-content 键指定需要启动服务扩展才能更新远程通知内容。 encrypted-content 键保存服务扩展可以在向用户呈现之前解密的加密数据。

查看以下示例服务扩展:

using UserNotification;

namespace myApp {
    public class NotificationService : UNNotificationServiceExtension {

        public override void DidReceiveNotificationRequest(UNNotificationRequest request, contentHandler) {
            // Decrypt payload
            var decryptedBody = Decrypt(Request.Content.UserInfo["encrypted-content"]);

            // Modify Notification body
            var newContent = new UNMutableNotificationContent();
            newContent.Body = decryptedBody;

            // Present to user
            contentHandler(newContent);
        }

        public override void TimeWillExpire() {
            // Handle out-of-time fallback event
            ...
        }

    }
}

此代码从 encrypted-content 键解密加密内容,创建新的 UNMutableNotificationContent,将 Body 属性设置为解密内容,并使用 contentHandler 向用户显示通知。

总结

本文介绍了 iOS 10 增强用户通知的所有方法。 它介绍了新的用户通知框架,以及如何在 Xamarin.iOS 应用或应用扩展中使用它。