about 3 years ago

iOS 10 讓我更接近了 Android

因為工作需求,摸索了一下 iOS 10 Notification 相關的一些變化,與實作功能的研究,也閱讀了許多網路分享的資料,將心得記錄在此。
由於之前都沒有實際開發過 Notification 相關的功能,所以直接學習 iOS 10 新支援的 framework,有些功能在 iOS8 就有支援,但是採用的 App 似乎不多,因此感覺都很新奇。
除了新功能之外,也有許多的改變勢將過往的功能整合起來,不會像以前一樣零散,至少我覺得學習起來蠻容易上手的。

Apple Developer Doc
Local and Remote Notification Programming Guide
活久见的重构 - iOS 10 UserNotifications 框架解析

被移除的元件

- UILocalNotification
- UIUserNotificationSettings
- UIUserNotificationCategory(UIMutableUserNotificationCategory)
- UIUserNotificationAction(UIMutableUserNotificationAction)
- handleActionWithIdentifier:forLocalNotification:
- handleActionWithIdentifier:forRemoteNotification:
- didReceiveLocalNotification:withCompletion:
- didReceiveRemoteNotification:withCompletion:

初始設定

  1. 加入 UserNotifications.framework
  2. 使用 @import UserNotifications;

向使用者申請權限

// Swift
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {
    granted, error in
    if granted {
        // 用户允许进行通知
    }
}

// Objective-C
[center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert + UNAuthorizationOptionSound + UNAuthorizationOptionBadge)
                              completionHandler:^(BOOL granted, NSError * _Nullable error) {
                                  // Enable or disable features based on authorization.
                                  // granted = YES,取得權限
                              }];

向 APNS 要求 token

利用 registerForRemoteNotifications 索取,並且在 didRegisterForRemoteNotificationsWithDeviceToken: 得到 deviceToken。 (這一步與iOS9以前一樣)

// Swift
UIApplication.shared.registerForRemoteNotifications()
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    let tokenString = deviceToken.hexString
    print("Get Push token: \(tokenString)")
}

// Objective-C
[[UIApplication sharedApplication] registerForRemoteNotifications];
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken;

Notification 權限檢查

為了避免重要的服務,一定要開啟 Notification,我們也可以檢查看看當前的設定是否正確。

// Swift
UNUserNotificationCenter.current().getNotificationSettings {
    settings in 
    print(settings.authorizationStatus) // .authorized | .denied | .notDetermined
    print(settings.badgeSetting) // .enabled | .disabled | .notSupported
    // etc...
}

// Objective-C
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
[center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
    //settings.authorizationStatus == UNAuthorizationStatusAuthorized)
}];

Notification Request 基本概念

UNNotificationRequest 包含

  • identifier(標記此request,未來可用於更新、刪除)
  • content,
  • trigger(本地端:TimeInterval、Calendar、Location,遠端只能馬上顯示)

然後利用 UNUserNotificationCenter 發送 UNNotificationRequest

APNS 的格式

{
  "aps":{
    "alert":{
      "title":"I am title",
      "title-loc-key":"XXXXX",  // 搭配多國語言檔使用
      "title-loc-args":"XXXXX", // 可以成為 title-loc-key 的 %@ 變數值
      "subtitle":"I am subtitle",
      "body":"I am body",
      "loc-key" : "GAME_PLAY_REQUEST_FORMAT",   // 搭配多國語言檔使用
            "loc-args" : [ "Jenna", "Frank"]        // 可以成為 loc-key 的 %@ 變數值
    },     
    "category":"saySomething",                              // 對應一個 category 的 identify (按鈕與自定UI需要)
    "sound":"default",
    "badge":1,
    "mutable-content":1                                             // for Service Extension
  }
  "file1" : "https://xxxx"      // 可以作為顯示 Attachment 的路徑
}

取消和更新

利用 identifier(遠端 apns-collapse-id)來針對發送過的訊息進行處理

// 本地端
    // 針對已傳送
    UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [identifier])
    // 針對等待中
  UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [identifier])

目前因為公司的 Server 使用 Amazon SNS,而 apns-collapse-id 是在 header,可能需要 Amazon 支援。

對訊息做出對應的動作的 delegate

// Swift
UNUserNotificationCenterDelegate 
    userNotificationCenter(_:didReceive:withCompletionHandler:)
  userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) 

因為涉及打開APP的反應,所以必須在 applicationDidFinishLaunching: 之前就要設定好 delegate。

// 設定 delegate 範例
class AppDelegate: UIResponder, UIApplicationDelegate {
    let notificationHandler = NotificationHandler()
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        UNUserNotificationCenter.current().delegate = notificationHandler
        return true
    }
}

利用 response: UNNotificationResponse 可以接收由訊息來的一些資料。

// 發送時
let content = UNMutableNotificationContent()
content.title = "Time Interval Notification"
content.body = "My first notification"
content.userInfo = ["name": "onevcat"]

// 接收時
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
    if let name = response.notification.request.content.userInfo["name"] as? String {
        print("I know it's you! \(name)")
    }
    completionHandler()
}

Actionable Notification 訊息按鈕

針對有自定按鈕的訊息,需利用 Categories 來做設計。
Local的使用 CategoryIdentifier 來指定,Remote的使用 category 來指定。

透過建立Notification Category來自定一個 Action:

// Swift
UNNotificationCategory(
    identifier:"saySomethingCategory",                                      // 標示
  actions: [inputAction, goodbyeAction, cancelAction],  // 動作設定,此為一個Text Input Action
  intentIdentifiers: [], 
  options: [.customDismissAction]
)
UNUserNotificationCenter.current().setNotificationCategories([傳入UNNotificationCategory變數]);

// Objective-C
UNNotificationCategory* categoryDEMO = [UNNotificationCategory categoryWithIdentifier:@"SomeID" actions:@[Action1, Action2] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction];
[center setNotificationCategories:@[categoryDEMO,categoryDEMO2]];

使用者點選後,觸發 userNotificationCenter:didReceiveNotificationResponse:

// Swift
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        //response.notification.request.content 可以得到 content
    //                              content.title
    //                              content.userInfo
    //response.actionIdentifier 可以得到 被選擇的Action的identifier
}

// Objective-C
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
         withCompletionHandler:(void(^)())completionHandler {
    NSString* actionIdentifier = response.actionIdentifier;
    DLog(@"Tapped in notification by %@", actionIdentifier);
    // Must be called when finished
    completionHandler();
}

Notification Extension

Notification extension 有兩種:
Service Extension:前者可以在收到 Remote Push 時,可以先做處理(30s)
Content Extension:可以用来自定通知的樣式

Service Extension

當我們想要對遠端訊息做一些操作,例如:加入 attachments、加解密,我們必須先對訊息進行計算的時候,我們可以使用 Service Extension來達到目的。
訊息必須符合:

  1. 需要顯示 alert 的遠端訊息
    1. aps 訊息中包含 mutable-content : 1

利用新增內建的樣板設計步驟如下:

  1. 新增 Target 並使用 Notification Service Extension當做樣板。(當初在這卡很久!)
  2. 即可發現xcode新增好一個資料夾與程式檔案
  3. 新增的Template已經幫你寫好,將title多加一個 [modified]
  4. 在將 push message 加入"mutable-content" : "1"
  5. 原則上就會看到接收到的訊息被改變了!
  6. Push Message 中在 apns:{} 外的值,可以在程式中使用 [xxx.userInfo objectForKey:@"image"]取得 Push 過來的值。

必須要實作的 delegate

didReceiveNotificationRequest:withContentHandler: // 訊息進入後的觸發點
serviceExtensionTimeWillExpire  // 如果在didReceive的執行時間到達限制時,則會啟動這個函式,讓你在顯示訊息前做最後修改。

Info.plist file 會自動建立,基本上不需要多做修改即可使用。以下有兩個比較關鍵的值:
NSExtensionPointIdentifier key to com.apple.usernotifications.service
NSExtensionPrincipalClass key to the name of your UNNotificationServiceExtension subclass

PS. 遇到的問題:

  1. 原本是用 iPhone5 做開發,雖然有出現[modified],但是隨後的修改都沒有效果。 後來改用 iPhone6 來開發,就可以成功了。
  2. 下載的連結檔案需要符合 https
Content Extension

利用 Xcode 的模板建立 UNNotificationContentExtension 的 UIViewController 子類別。
一樣也是利用類似 Service Extension 的建立方式,加入新的 Target。
範例會自動建立兩個method:

- (void)viewDidLoad;
- (void)didReceiveNotification:(UNNotification *)notification;

一樣的也有 Info.plist 但是相較於 Service Extension,Content Extension可以做的變化比較多,有三項如下:

UNNotificationExtensionCategory // 設定 Category 名稱,需要與 aps 中的設定一樣
UNNotificationExtensionInitialContentSizeRatio // 系統在載入時等待畫面,預設的長寬比例
UNNotificationExtensionDefaultContentHidden // 當顯示出 Content UI時,是否要隱藏原本的"title, body"

PS. 如何在 extension 部分做 debug?
在 Run 時,選擇你要跑的 extension,xcode會詢問你要 choose An app,然後再選你的 app,即可有 log 與 breakpoint。
(雖然記得剛開始使用過這樣的作法,但是不成功,可是後來查到這個作法後,的確可行!)

PS. 在 extension 使用 App 現有的圖片資源
可能是因為 Extension Bundle 與 main Bundle 的不同,所以需要設定 Copy Bundle Resource
可以新增一個 Asset Catagory,加入需要的圖片再加入 Bundle Resource

UNNotificationAttachment

讓訊息可以夾帶 聲音檔(Audio 5MB)、圖片(Image 10MB)、影片(Movie 50MB),但是限制使用手機中的檔案。
本地端使用時只要 UNMutableNotificationContent 中的 attachments 設定好物件 UNNotificationAttachment 即可。
遠端的通知訊息則需要靠 UNNotificationServiceExtension 來做下載動作,再放入 attachments。
NOTE:

  1. extension bundle 與 main bundle是不同的!
  2. UNNotificationAttachmentOptionsThumbnailClippingRectKey 設定的預覽圖 UNNotificationAttachmentOptionsThumbnailTimeKey 設定 gif 或是影片的預覽圖
  3. attachments 為一個陣列,但是只會顯示第一個。 在要客製訊息UI時,則可以顯示多個attachment
  4. 在建立 UNNotificationAttachment 時,如果 URL 的檔案還沒有準備好,會直接有 Error (Code:100)
    UNNotificationAttachment
    identifier 物件的ID URL 物件的位置 type 檔案類型

使用 Amazon SNS 做實驗

登入SNS並且完成閒置步驟後(未實際操作過),從
- (void)application: didRegisterForRemoteNotificationsWithDeviceToken:
取得device toen

  1. 在 SNS/Applications/Endpoints 中搜尋出 Toekn
  2. 選擇 Publish to endpoint
  3. 選擇 JSON
  4. 輸入 Message (需要加入 \",不可換行)
    {"default":"This is the default Message","APNS_SANDBOX":"{ \"aps\" : { \"alert\" : {\"title\":\"You have got title.\",\"body\":\"You have got email.\"}, \"badge\" : 9,\"catgory\":\"FF01|25\",\"mutable-content\":1,\"sound\" :\"default\"}}"}
    
    另外一個工具 Knuff Knuff可以使用正常的格式,從 {"aps":"xxxxx"}開始,無需加"\"
← 五個為什麼? iOS 10 Widget →