《iOS面试之道》读书笔记 - UserNotifications框架概况

UserNotifications 框架可以用来从应用程序本地生成通知,也可以从你的服务器远程生成通知。对于本地通知(local notifications),应用程序会创建通知内容,并指定触发通知传递的条件,如时间或位置。对于远程通知(remote notifications)(也称为推送通知),使用某个服务器生成通知,Apple推送通知服务(APN)处理将这些通知传递到用户的设备[1]

请求使用权限

请用户允许显示警报(alerts),播放声音或标记应用程序图标(badge)以响应通知。

本地和远程通知通过alert,声音或badge引起用户的注意。当app未运行或处于后台时,仍然会发生这些互动。通知让用户知道app具有相关信息供他们查看。由于用户可能会认为基于通知​​的交互具有破坏性,因此必须显式地获得使用通知的权限[2]

在app启动时请求授权

在app启动时发起授权请求,在代码中获取UNUserNotificationCenter对象,并调用requestAuthorizationWithOptions:completionHandler:方法,指定app使用的所有交互类型。

UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert | UNAuthorizationOptionBadge | UNAuthorizationOptionSound) 
                      completionHandler:^(BOOL granted, NSError * _Nullable error) {
                          // 基于用户的授权状态打开或关闭app的相关功能
                      }];

App首次发出此授权请求时,系统会提示用户授予或拒绝该请求并记录用户的响应。后续授权请求不会提示用户。

根据授权配置支持的通知类型

在试图发起本地通知之前,请确保app已获得授权。即使最初获取了授权,用户也可以随时取消app的授权设置。用户也可以改变允许的交互类型,这可能回导致你改变通知的配置。

使用授权设置能够为用户提供更好的体验。即使你在UNMutableNotificationContent对象中指定了适当的信息,如果app没有获取相应的授权,系统也不会执行交互。调用UNUserNotificationCentergetNotificationSettingsWithCompletionHandler:方法以获取当前的授权配置。

UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
    if (settings.authorizationStatus != UNAuthorizationStatusAuthorized) {
        return;
    }
    
    if (settings.alertSetting == UNNotificationSettingEnabled) {
        // 发送alert
        [self myScheduleAlertNotification];
    }
    else {
        // 发送声音和badge
        [self badgeAppAndPlaySound];
    }
}];

即使app未获取特定的授权,你也可能希望使用相关信息配置通知。如果UNNotificationSettingsnotificationCenterSetting属性设置为UNNotificationSettingEnabled,则通知中心仍会显示包含alert的通知。同时当app处于前台时,通知也会传递到userNotificationCenter:willPresentNotification:withCompletionHandler:方法。

从app发起本地通知

当你想引起用户的注意时,请从app创建并安排通知。

系统会根据你指定的时间或地点传递和处理通知。如果在app未运行或在后台运行通知时,系统会与用户进行交互。如果app位于前台,系统会将通知发送到app以进行处理[3]

创建通知的内容(Notification Content)

使用UNMutableNotificationContent对象表示通知内容。

UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = @"Weekly Staff Meeting";
content.body = @"Every Tuesday at 2pm";

指定触发条件(Notification Trigger)

为通知指定触发条件,使用UNCalendarNotificationTriggerUNTimeIntervalNotificationTriggerUNLocationNotificationTrigger对象。不同的 trigger 需要使用不同的参数,例如基于日历的 trigger 需要指定触发日期和时间。
下面的代码展示了如何配置每周二下午2点发送的通知。NSDateComponents对象指定事件的触发时间。配置 trigger 的repeats属性会使系统在传递事件后重新规划该事件,从而实现周期性触发通知的效果。

NSDateComponents *components = [[NSDateComponents alloc] init];
components.calendar = [NSCalendar currentCalendar];
components.weekday = 3;
components.hour = 14;

UNCalendarNotificationTrigger *trigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:components repeats:YES];

创建并注册通知请求(Notification Request)

创建一个包含内容(content)和触发条件(trigger)的UNNotificationRequest对象,并调用addNotificationRequest:withCompletionHandler:方法使系统规划你的请求(request)。

// Create the request
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:[NSUUID UUID] content:content trigger:trigger];

// Schedule the request with the system
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
    if (error) {
        // Handle any errors
    }
}];

取消已规划的请求

一旦被规划,通知请求(Notification Request)将保持活动状态,直到满足其触发条件,或被显式地取消。一般来说,你可以在条件改变时取消通知,而不再需要通知用户。取消一个活动的通知请求,需要调用UNUserNotificationCenterremovePendingNotificationRequestsWithIdentifiers:removeAllPendingNotificationRequests方法。

向APNs注册你的app

与 APNs 通信并接收标识 app 的唯一设备令牌(unique device token)

APNs 必须先知道用户设备的地址,然后才能向该设备发送通知。此地址采用设备令牌(device token)的形式,该设备令牌对于设备和应用程序都是唯一的。在启动时,app 与 APNs 通信并接收其t oken,然后将其转发到你的业务服务器。你的服务器包含该 token 及其发送的任何通知。

注意,即使两个 app 都安装在同一设备上,一个 app 的 token 也无法用于其他 app。两个 app 都必须请求自己唯一的 token 并将其转发到业务服务器[4]

启用推动通知功能

要推送通知功能,app 必须具有合适的 entitlement。要将这些 entitlements 添加到 app,需要在Xcode项目中启用推送通知功能,如下图所示。在 iOS 中启用此选项会将 aps-environment 代码签名 entitlement 添加到 app 中。

《iOS面试之道》读书笔记 - UserNotifications框架概况_第1张图片
启用推送通知功能

注意,在你的开发者帐户中,你还必须启用该项目的App ID推送通知服务。有关配置开发者帐户的详细信息,请转到开发者帐户页面。

注册你的app并获取应用程序的设备令牌(Device Token)

使用 APNs 注册你的 app 并接收全局唯一设备令牌(Globally Unique Device Token),该 token 是当前设备上你 app 的有效地址。业务服务器必须先具有此 token 才能向设备发送通知。
每次使用苹果提供的API(Apple-provided APIs)启动 app 时,都需注册 app 并接收 token。注册过程在各个平台上类似:

  • 在iOS和tvOS中,调用UIApplication对象的registerForRemoteNotifications方法用来请求 token。注册成功后,你将在application:didRegisterForRemoteNotificationsWithDeviceToken:方法中收到 token。
  • 在 macOS 中,调用NSApplication对象的registerForRemoteNotificationTypes:方法用来请求 token。注册成功后,你将在application:didRegisterForRemoteNotificationsWithDeviceToken:方法中收到 token。
  • 在 watchOS 中,不需要显式地注册远程通知。用户的 iPhone 会在适当的时间自动将远程通知转发给watchOS应用程序。

除了处理成功注册 APNs 之外,还要实现application:didFailToRegisterForRemoteNotificationsWithError:方法,处理失败的注册。如果用户的设备未连接到网络,APNs 服务器因任何原因无法访问,或者应用程序没有正确的代码签名权限,则注册可能会失败。发生故障时,设置一个标志并尝试稍后再次注册。

下面代码实现了远程通知和接收相应 token 所需的委托方法。sendDeviceTokenToServer方法是 app 用于将数据发送到其提供程序服务器的自定义方法。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch
    
    [[UIApplication sharedApplication] registerForRemoteNotifications];
    return YES;
}

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    [self sendDeviceTokenToServer:deviceToken];
}

- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
    // Try again later
}

注意,切勿在本地存储中缓存 token。当用户从备份还原设备,用户在新设备上安装 app 以及用户重新安装操作系统时,APNs 会发出新 token。如果你要求系统每次都提供令牌,则可以保证获得最新的令牌。

将token转发到业务服务器

获取 token 后,打开从应用程序到提供商服务器的网络连接。安全地转发 token 以及识别服务器特定用户所需的任何其他信息。例如,可以包含用户的登录名或将其连接到业务服务的内容。加密任何通过网络发送的信息。
在业务服务器上,将令牌存储在安全的位置,您可以在其中访问它们以发送通知。生成通知时,你的服务器必须能够向特定设备发送通知。因此,如果通知链接到用户的帐户,请使用用户的帐户信息存储设备令牌。由于用户可以拥有多个设备,因此需要处理多个设备令牌。

如需获取如何将 payload 和 token 发送到 APNs 的信息,参考向APNs发送通知请求。

声明可操作的通知类型

区分通知并将操作按钮添加到通知界面

可操作的通知(Actionable notifications)允许用户响应已发送的通知,而无需启动相应的app。其他通知在通知界面中显示信息,但用户唯一的操作方法是启动app。对于可操作的通知,除通知界面外,系统还显示一个或多个按钮。点击按钮会将所选操作发送到app,然后app在后台处理操作[5]

声明自定义操作(Custom Action)和通知类型(Notification Type)

必须在 app 启动时声明支持的所有操作(action)。你可以联合使用类别动作对象来声明一个操作:使用UNNotificationCategory对象定义 app 支持的通知类型,使用UNNotificationAction对象定义要为每种类型显示的按钮。例如,一个会议邀请的通知可能包括接受或拒绝邀请的按钮。

每一个UNNotificationCategory对象都有一个唯一标识和用于处理该类型通知的选项。identifier属性中的字符串是类别对象中最重要的部分,生成通知时,必须在通知的有效载荷(payload)中包含相同的字符串。系统使用该字符串来定位相应的类别对象和任何操作。

要将操作与通知类别相关联,请为其分配一个或多个UNNotificationAction对象。每个操作对象都包含要显示给用户的本地化字符串以及表示你希望如何处理该操作的选项。例如,当您将操作标记为破坏性(destructive)时,系统会以不同的高亮形式显示该操作以指示其行为。

下面的代码展示了如何使用两个操作注册自定义类别。除标题和选项外,每个操作都有唯一的标识符。当用户选择操作时,系统会将该标识符传递给app。

UNNotificationAction *acceptAction = [UNNotificationAction actionWithIdentifier:@"ACCEPT_ACTION" 
                                                                          title:@"Accept" 
                                                                        options:UNNotificationActionOptionNone];
UNNotificationAction *declineAction = [UNNotificationAction actionWithIdentifier:@"DECLINE_ACTION" 
                                                                           title:@"Decline" 
                                                                         options:UNNotificationActionOptionNone];

// Define the notification type
UNNotificationCategory *meetingInviteCategory = [UNNotificationCategory categoryWithIdentifier:@"MEETING_INVITATION" 
                                                                                       actions:@[acceptAction, declineAction] 
                                                                             intentIdentifiers:@[] 
                                                                 hiddenPreviewsBodyPlaceholder:@"" 
                                                                                       options:UNNotificationCategoryOptionCustomDismissAction];

// Register the notification type
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center setNotificationCategories:[NSSet setWithObject:meetingInviteCategory]];

注意:所有操作(action)对象都必须具有唯一标识符。处理操作时,标识符是区分一个操作与另一个操作的唯一方法,即使这些操作属于不同的类别。

大多数操作只会让用户选择,但文本输入操作也允许用户键入基于自定义文本的响应(response)。然后,app可以将用户输入的响应合并到您对操作的处理中。例如,一个聊天app可以将键入的文本作为对传入消息的响应。要创建文本输入操作,需要创建UNTextInputNotificationAction对象而不是UNNotificationAction对象。当用户点击输入按钮时,系统显示可编辑的文本字段(test field)。当系统向app报告该操作时,系统会包含用户在响应中键入的文本。

在有效载荷(Payload)中包含通知类别(Notification Category)

系统仅显示那些payload包含有效类别标识符字符串(Category Identifier)的通知的操作。系统使用类别标识符来查找应用程序的已注册类别及其相关操作。然后,它使用该信息将操作按钮添加到通知界面。系统使用类别标识符来查找应用程序的已注册类别及其相关操作。然后,它使用该信息将操作按钮添加到通知界面。

下面的代码展示了如何创建一条会议邀请本地通知的内容(content)。要将类别分配给本地通知,需要将相应的字符串赋值给UNMutableNotificationContent对象的categoryIdentifier属性。除了基本信息之外,此代码还会将自定义数据添加到通知的userInfo字典中,稍后将使用该字典来处理该会议邀请。

UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = @"Weekly Staff Meeting";
content.body = @"Every Tuesday at 2pm";
content.userInfo = @{
    @"MEETING_ID": meetingID,
    @"USER_ID": userID
};
content.categoryIdentifier = @"MEETING_INVITATION";

如果是在远程通知中添加类别标识符,需要将category字段添加到 JSON payload的aps字典中。

{
   "aps" : {
      "category" : "MEETING_INVITATION"
      "alert" : {
         "title" : "Weekly Staff Meeting"
         "body" : "Every Tuesday at 2pm"
      },
   },
   "MEETING_ID" : "123456789",
   "USER_ID" : "ABCD1234"
}

处理选定的操作

App必须处理它定义的所有操作。当用户选择一个操作时,系统会在后台启动 app 并通知共享的UNUserNotificationCenter对象实例,该对象会通知其代理对象(delegate)。使用代理对象的userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:方法来识别所选操作并提供适当的响应。

下面的代码实现了管理会议邀请的 app 的委托方法。该方法使用响应的actionIdentifier属性来确定是接受还是拒绝给定的邀请。它还依赖于通知有效载荷中的自定义数据来成功处理通知。完成处理操作后,应当始终调用完成处理程序(completion handler)

- (void)userNotificationCenter:(UNUserNotificationCenter *)center 
didReceiveNotificationResponse:(UNNotificationResponse *)response 
         withCompletionHandler:(void(^)(void))completionHandler {

    // Get the meeting ID from the original notification
    NSDictionary *userInfo = response.notification.request.content.userInfo;
    NSString *meetingID = userInfo[@"MEETING_ID"];
    NSString *userID = userInfo[@"USER_ID"];

    // Perform the task associated with the action
    if ([response.actionIdentifier isEqualToString:@"ACCEPT_ACTION"]) {
        [sharedMeetingManager acceptMeeting:meetingID forUser:userID];
    }
    else if ([response.actionIdentifier isEqualToString:@"DECLINE_ACTION"]) {
        [sharedMeetingManager declineMeeting:meetingID forUser:userID];
    }
    else {
        // Handle other actions…
    }

    // Always call the completion handler when done
    completionHandler();
}

处理通知和与通知相关的操作

响应用户与系统通知界面的交互,包括处理应用程序的自定义操作

通知(Notifications)是将信息呈现在用户面前的一种主要方式,同时 app 本身也可以响应这些通知。例如 app 可以响应这些操作[6]

  • 用户从通知界面中选择的操作
  • App 在前台运行时收到的通知
  • 静默通知
  • 与PushKit相关的通知

处理 app 在前台运行时的通知

如果 app 在前台运行时收到通知,系统会将该通知直接发送到 app。收到通知后,可以使用通知的有效负载(Payload)执行任何操作。例如,你可以更新 app 的界面以反映通知中包含的新信息。然后,你可以禁止或修改任何已规划的 alert。
当通知到达时,系统调用UNUserNotificationCenter委托对象(delegate)userNotificationCenter:willPresentNotification:withCompletionHandler:方法。使用该方法处理通知,并让系统知道你希望如何继续。
下面的代码实现了该方法。当会议邀请到达时,app 会调用其自定义的queueMeetingForDelivery方法在 app 界面中显示新邀请。App 还要求系统通过将值UNNotificationPresentationOptionSound传递给完成处理程(completion handler)来播放通知的声音。对于其他通知类型,该方法使通知静音。

- (void)userNotificationCenter:(UNUserNotificationCenter *)center 
       willPresentNotification:(UNNotification *)notification 
         withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler {
    
    if ([notification.request.content.categoryIdentifier isEqualToString:@"MEETING_INVITATION"]) {
        // Retrieve the meeting details
        NSString *meetingID = userInfo[@"MEETING_ID"];
        NSString *userID = userInfo[@"USER_ID"];

        // Add the meeting to the queue
        [sharedMeetingManager queueMeetingForDelivery:meetingID forUser:userID];

        // Play a sound to let the user know about the invitation
        completionHandler(UNNotificationPresentationOptionSound);
        return;
    }
    else {
        // Handle other notification types...
    }

    // Don't alert the user for other types
    completionHandler(UNNotificationPresentationOptionNone);
}

如果你使用 PushKit 注册了应用,则针对 PushKit 类型的通知始终会直接发送到您的应用,并且永远不会向用户显示。如果 app 位于前台或后台,系统会为 app 提供处理通知的时间。如果 app 未运行,系统会在后台启动 app,以便它可以处理通知。要发送 PushKit 通知,你的服务器必须将通知的主题设置为适当的目标,例如您的应用程序的复杂性。有关注册PushKit通知的更多信息,请参阅PushKit。

修改通知中的内容

在用户的iOS设备上显示之前修改远程通知的有效负载(Payload)

如果需要,你可能要在用户的iOS设备上修改远程通知的内容:

  • 解密以加密格式发送的数据。
  • 下载大小超过最大有效载荷大小的图像或其他媒体附件。
  • 更新通知的内容,可能是通过合并用户设备的数据。

修改远程通知需要通知服务应用程序扩展(service app extension),您可以将其包含在iOS应用程序包(bundle)中。应用程序扩展在向用户显示之前会接收远程通知的内容,从而在用户收到alert之前更新通知的有效负载(Payload)。你可以控制扩展程序处理哪些通知。

注意,通知服务应用程序扩展仅对配置为向用户显示alert的远程通知进行操作。如果您的应用程序禁用了alert,或者有效负载仅指定播放声音或图标的标记,则扩展程序不生效[7]

为项目添加服务应用程序扩展(Service App Extension)

通知服务应用扩展程序在你的iOS应用中作为单独的捆绑包发布。要将此扩展程序添加到您的应用中:

  1. 在Xcode中选择 File > New > Target。
  2. 从 iOS > Application section 中选择 Notification Service Extension。
  3. 点击 Next。
  4. 指定应用扩展程序的名称和其他配置详细信息。
  5. 点击 Finish。

实现扩展的方法

Xcode提供的通知服务扩展模板包含了一个可供修改的默认实现:

  • didReceiveNotificationRequest:withContentHandler:方法可以使用更新的内容创建UNMutableNotificationContent对象。
  • 使用serviceExtensionTimeWillExpire方法终止仍在运行的任何有效载荷修改任务。

didReceiveNotificationRequest:withContentHandler:方法大概只有30秒的时间来修改有效负载并调用提供的完成处理程序(completion handler)。如果你的代码需要更长的时间,系统将调用serviceExtensionTimeWillExpire方法,此时你必须立即向系统返回任何内容。如果你无法从任一方法调用完成处理程序,系统将显示通知的原始内容。

下面的代码实现了UNNotificationServiceExtension对象,该对象解密使用远程通知传递的加密消息的内容。didReceiveNotificationRequest:withContentHandler:方法解密数据并在通知内容成功时返回修改后的内容。如果不成功,或者时间到期,则扩展程序返回指示数据仍处于加密状态的内容。

// Storage for the completion handler and content
@interface NotificationService ()
@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
@end

@implementation NotificationService
// Modify the payload contents
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request 
                   withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
    self.contentHandler = contentHandler;
    self.bestAttemptContent = [request.content mutableCopy];

    // Try to decode the encrypted message data
    NSData *encryptedData = bestAttemptContent.userInfo[@"ENCRYPTED_DATA"] ?: [NSData data];
    NSString *decryptedMessage = [self decript:encryptedData];
    bestAttemptContent.body = decryptedMessage ?: @"(Encrypted)";

    // Always call the completion handler when done
    contentHandler(bestAttemptContent)
}

// Return something before time expires
- (void)serviceExtensionTimeWillExpire {
    // Mark the message as still encrypted
    bestAttemptContent.subtitle = @"(Encrypted)";
    bestAttemptContent.body = @"";
    contentHandler(bestAttemptContent);
}
@end

配置远程通知的有效负载

仅当远程通知的有效负载包含以下信息时,系统才会执行通知内容应用程序扩展:

  • 有效负载必须包含mutable-content值为1
  • 有效负载必须包含alert字典,包含titlesubtitlebody

下面的JSON数据展示了包含加密数据的通知 payload。设置mutable-content值为1,以便用户的设备知道运行相应的服务应用程序扩展。

{
   "aps" : {
      "category" : "SECRET",
      "mutable-content" : 1,
    "alert" : {
         "title" : "Secret Message!",
         "body"  : "(Encrypted)"
     },
   },
   "ENCRYPTED_DATA" : "Salted__·öîQÊ$UDì_¶Ù∞è   Ω^¬%gq∞NÿÒQùw"
}

  1. UserNotifications ↩

  2. Asking Permission to Use Notifications ↩

  3. Scheduling a Notification Locally from Your App ↩

  4. Registering Your App with APNs ↩

  5. Declaring Your Actionable Notification Types ↩

  6. Handling Notifications and Notification-Related Actions ↩

  7. Modifying Content in Newly Delivered Notifications ↩

你可能感兴趣的:(《iOS面试之道》读书笔记 - UserNotifications框架概况)