iOS 遠端通知

weixin_34050427發表於2018-09-20

級別: ★★☆☆☆
標籤:「iOS通知」「iOSPush」「遠端通知」
作者: dac_1033
審校: QiShare團隊

iOS中的通知(Notification)分為兩種:
1. iOS 本地通知
2. iOS 遠端通知
3. iOS 通知擴充套件


iOS中的通知包括本地通知和遠端通知,兩種通知在iOS系統中通過橫幅或者彈出提醒兩種形式來告訴使用者,點選系統彈出的通知會開啟應用程式。

今天主要介紹iOS端關於遠端通知的相關功能及操作。

遠端通知

遠端通知是通過蘋果的APNsApple Push Notification server)傳送到App,而APNs必須先知道使用者裝置的地址,然後才能向該裝置傳送通知。此地址採用裝置令牌的形式,該裝置令牌對於裝置和應用程式都是唯一的(即device token)。在啟動時,AppAPNs通訊並接收device token,然後將其轉發到App ServerApp Server將包含該令牌及要傳送的通知訊息傳送至APNs。(蘋果官網APNs概述

遠端通知的傳遞涉及幾個關鍵元件:

  • App Server
  • Apple推送通知服務(APNs)
  • 使用者的裝置(包括iPhone、iPad、iTouch、mac等)
  • 相應的App

蘋果官方提供的遠端通知的傳遞示意圖如下:

13180946-e97aa8e8b5c242f1.png
遠端通知的傳遞示意圖

遠端通知中各關鍵元件之間的互動細節:

1977357-0880dab97509b3fb.png
遠端通知各元件之間的互動細節

準備工作:

(1)在蘋果開發者賬號中建立的App ID不能使用通配ID,並且在所建立的APP ID的配置項中選擇Push Notifications服務,App使用沒有選擇該服務的App ID所生成的推送證照和配置檔案時,無法完成註冊遠端通知;
(2)當前工程配置中的Bundle Identifier必須和生成配置檔案使用的APP ID完全一致;
(3)當前工程配置中的“Capabilities”需設定為ON
(4)遠端推送必須真機除錯,模擬器無法獲取得到device token

詳細步驟:

  1. 在AppDelegate中註冊APNs訊息
-(void)registerRemoteNotification {
    
    if ([[UIDevice currentDevice].systemVersion floatValue] >= 10.0) {
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 // Xcode 8編譯會呼叫
        if (@available(iOS 10.0, *)) {
            UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
            center.delegate = self;
            [center requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionCarPlay) completionHandler:^(BOOL granted, NSError *_Nullable error) {
                if (!error) {
                    NSLog(@"request notification authorization succeeded!");
                }
            }];
        }
        [[UIApplication sharedApplication] registerForRemoteNotifications];
#else // Xcode 7編譯會呼叫
        UIUserNotificationType types = (UIUserNotificationTypeAlert | UIUserNotificationTypeSound | UIUserNotificationTypeBadge);
        UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:types categories:nil];
        [[UIApplication sharedApplication] registerUserNotificationSettings:settings];
        [[UIApplication sharedApplication] registerForRemoteNotifications];
#endif
    } else {
        UIUserNotificationType types = (UIUserNotificationTypeAlert | UIUserNotificationTypeSound | UIUserNotificationTypeBadge);
        UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:types categories:nil];
        [[UIApplication sharedApplication] registerUserNotificationSettings:settings];
        [[UIApplication sharedApplication] registerForRemoteNotifications];
    }
}
  1. App獲取device token
  • 在註冊遠端通知之後,獲取device token成功回撥:
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken;
  • 獲取device token失敗:
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error;
  1. App將device token傳送給App Server
    只有蘋果公司知道device token的生成演算法,保證唯一,device token在App重灌等情況時會變化,因此為確保device token變化後App仍然能夠正常接收伺服器端傳送的通知,建議每次應用程式啟動都重新獲得device token,並傳給App Server

  2. App Server根據最新的device token將要推送的訊息傳送給APNs
    將指定device token和訊息內容傳送給APNs時,訊息內容的格式必須完全按照蘋果官方的訊息格式組織訊息內容,點選檢視遠端通知訊息的欄位建立遠端通知訊息

訊息格式的例子如下:
{"aps":{"alert":{"title":"通知的title","subtitle":"通知的subtitle","body":"通知的body","title-loc-key":"TITLE_LOC_KEY","title-loc-args":["t_01","t_02"],"loc-key":"LOC_KEY","loc-args":["l_01","l_02"]},"sound":"sound01.wav","badge":1,"mutable-content":1,"category": "realtime"},"msgid":"123"}

  1. APNs根據device token查詢相應裝置,並推送訊息
    一般情況APNs可以根據deviceToken將訊息成功推送到相應裝置中,但也存在使用者解除安裝程式等原因導致推送訊息失敗的情況,這時App服務端會收到APNs返回的錯誤資訊)。

  2. AppDelegate.m中的回撥方法

// iOS<10時,且app被完全殺死
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;

// 注:iOS10以上,如果不使用UNUserNotificationCenter,將走此回撥方法
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo;

// iOS7及以上系統
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler;

//  iOS>=10: App在前臺獲取到通知
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler;

//  iOS>=10: 點選通知進入App時觸發(殺死/切到後臺喚起)
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler;

註冊遠端通知及解析通知資料的程式碼如下:

#import "AppDelegate.h"
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
#import <UserNotifications/UserNotifications.h>
#endif

@interface AppDelegate () <UNUserNotificationCenterDelegate>
@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
   
    // 註冊APNs
    [self registerRemoteNotifications];
    
    return YES;
}

- (void)registerRemoteNotifications {
    if ([[UIDevice currentDevice].systemVersion floatValue] >= 10.0) {
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 // Xcode 8編譯會呼叫
        if (@available(iOS 10.0, *)) {
            UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
            center.delegate = self;
            [center requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionCarPlay) completionHandler:^(BOOL granted, NSError *_Nullable error) {
                if (!error) {
                    NSLog(@"request authorization succeeded!");
                }
            }];
        } else {
            // Fallback on earlier versions
        }
        
        [[UIApplication sharedApplication] registerForRemoteNotifications];
#else // Xcode 7編譯會呼叫
        UIUserNotificationType types = (UIUserNotificationTypeAlert | UIUserNotificationTypeSound | UIUserNotificationTypeBadge);
        UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:types categories:nil];
        [[UIApplication sharedApplication] registerUserNotificationSettings:settings];
        [[UIApplication sharedApplication] registerForRemoteNotifications];
#endif
    } else if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0) {
        UIUserNotificationType types = (UIUserNotificationTypeAlert | UIUserNotificationTypeSound | UIUserNotificationTypeBadge);
        UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:types categories:nil];
        [[UIApplication sharedApplication] registerUserNotificationSettings:settings];
        [[UIApplication sharedApplication] registerForRemoteNotifications];
    } else {
        UIRemoteNotificationType apn_type = (UIRemoteNotificationType)(UIRemoteNotificationTypeAlert |
                                                                       UIRemoteNotificationTypeSound |
                                                                       UIRemoteNotificationTypeBadge);
        [[UIApplication sharedApplication] registerForRemoteNotificationTypes:apn_type];
    }
}

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {

    // 獲取並處理deviceToken
    NSString *token = [[deviceToken description] stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]];
    token = [token stringByReplacingOccurrencesOfString:@" " withString:@""];
    DLog(@"---DeviceToken--->> %@\n", token);
}

- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
    DLog(@"---register RemoteNotifications failed---\n%@", error);
}

// 注:iOS10以上,如果不使用UNUserNotificationCenter,將走此回撥方法
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
    // iOS6及以下系統
    if (userInfo) {
        if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) {// app位於前臺通知
            NSLog(@"app位於前臺通知(didReceiveRemoteNotification:):%@", userInfo);
        } else {// 切到後臺喚起
            NSLog(@"app位於後臺通知(didReceiveRemoteNotification:):%@", userInfo);
        }
    }
}

// 注:
// 1. 該回撥方法,App殺死後並不執行;
// 2. 該回撥方法,會與application:didReceiveRemoteNotification:互斥執行;
// 3. 該回撥方法,會與userNotificationCenter:willPresentNotification:withCompletionHandler:一併執行;
// 4. 該回撥方法,會與userNotificationCenter:didReceiveNotificationResponse::withCompletionHandler:一併執行。
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler NS_AVAILABLE_IOS(7_0) {
    // iOS7及以上系統
    if (userInfo) {
        if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) {// app位於前臺通知
            NSLog(@"app位於前臺通知(didReceiveRemoteNotification:fetchCompletionHandler:):%@", userInfo);
        } else {// 切到後臺喚起
            NSLog(@"app位於後臺通知(didReceiveRemoteNotification:fetchCompletionHandler:):%@", userInfo);
        }
    }
    completionHandler(UIBackgroundFetchResultNewData);
}

#pragma mark - iOS>=10 中收到推送訊息

#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
//  iOS>=10: App在前臺獲取到通知
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
API_AVAILABLE(ios(10.0)) {
    NSDictionary * userInfo = notification.request.content.userInfo;
    if (userInfo) {
        NSLog(@"app位於前臺通知(willPresentNotification:):%@", userInfo);
    }
    completionHandler(UNNotificationPresentationOptionBadge|UNNotificationPresentationOptionSound|UNNotificationPresentationOptionAlert);;
}

//  iO>=10: 點選通知進入App時觸發(殺死/切到後臺喚起)
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler
API_AVAILABLE(ios(10.0)) {
    NSDictionary * userInfo = response.notification.request.content.userInfo;
    if (userInfo) {
        NSLog(@"點選通知進入App時觸發(didReceiveNotificationResponse:):%@", userInfo);
    }
    completionHandler();
}
#endif


@end
  1. 模擬推送工具“Pusher”
    本文只側重於介紹iOS端對遠端推送通知的處理,因此我們把App Server對應的處理過程交給了第三方工具,第三方推送測試工具有很多,如SmartPushPusher等,在這裡我們選用Pusher作為測試工具,Pusher的GitHub地址
    13180946-4983209fa7aeea00.png
    Pusher截圖

Pusher的使用步驟說明:
(1)選擇p12格式的推送證照;
(2)設定是否為測試環境(預設勾選為測試環境,由於推送證照分為測試推送證照和生產測試證照,並且蘋果的APNs也分為測試和生產兩套環境,因此Pusher需要手動置頂是否為測試環境);
(3)輸入device token
(4)輸入符合蘋果要求的推送內容字串;
(5)當確認手機端設定無誤,並且以上4點設定正確時,執行推送。
Pusher推送的訊息,以第4點中的示例為例進行測試,手機收到遠端推送通知的效果截圖如下:

13180946-51c7a5929f44af53.png
iOS遠端推送通知效果圖

點選遠端推送通知橫幅開啟App,在回撥中獲取的json串:
13180946-4828d7c1745d9050.png
點選橫幅,在App回撥方法中獲取資料

備註:
(1)要使用APNs向非執行的應用程式提供遠端通知,需要至少啟動目標應用程式一次;
(2)裝置沒有網路的情況下,是無法註冊遠端通知的;
(3)一般情況下,device token是不會發生變化的,即雖然呼叫註冊遠端通知的方法,但是返回的device token仍然是之前得到的值;如果裝置令牌在應用程式執行時發生更改,則應用程式物件再次呼叫相應的委託方法以通知更改;
(4)推送過程中的訊息json串可在適當位置新增自定義欄位,整個訊息最大長度為4 KB4096位元組),超過最大允許長度,則拒絕通知;
(5)在iOS及以上系統中遠端通知還包括“通知擴充套件”功能,在下一篇文章中介紹。

本文Demo連結:GitHub地址

專欄下一篇:iOS 通知擴充套件


關注我們的途徑有:
QiShare(簡書)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公眾號)

推薦文章:
在iOS 12中無法獲取WiFi的SSID了?別慌!
Web安全漏洞之CSRF
奇舞週刊276期

相關文章