[iOS 10 day by day] Day 5:新的通知推送 API

iOS 10 最重要的變化可能就是通知 API 的重構了。本文用一個簡單鬧鐘的例子介紹了 User Notification 的 API 變化和新功能。javascript

《iOS 10 day by day》是 shinobicontrols 公司編寫的系列博客,介紹開發者須要瞭解的 iOS 10 新特性,每週更新。本系列翻譯(文集地址)已取得官方受權。目錄點此。倉薯翻譯,歡迎指正:)java

Shinobicontrols 爲 iOS 和 Android 開發者提供高性能、響應式的 UI 控件 SDK,尤爲是圖表方面的控件。 官網 : shinobicontrols.com twitter : @shinobicontrolsios

簡介

好久之前,開發者就能夠在 iOS 裏預定本地通知了,可是以前的 API 缺少細粒度的控制能力。幸運的是,蘋果在 iOS 10 中改善了這一點,發佈了新的 UserNotifications 框架。這個框架在處理本地通知及遠程推送方面的 API 豐富了許多,同時寫法更加簡便。git

本地通知(local notification)是用 app 來預定的通知,例如:提醒你帶午餐的鬧鐘。而遠程推送(remote notification)通常是服務器發起的,傳到蘋果的 APNS 服務器上,APNS 再推送到用戶手機上。例如:推送給全部用戶,告訴他們 app 發佈新版本了。github

實例工程

工程是用 Xcode 8 Beta 6 建的api

咱們用一個簡單的鬧鐘 app 來介紹新的 UserNotification 框架,一個用戶能夠預定提醒的 to do list。到時間後,鬧鐘每 60 秒提醒一次,直到用戶手動取消爲止。跟以前同樣,代碼放在 github 上數組

每一個小喇叭的圖標表示一個預定好的提醒,而被紅色斜槓劃掉的小喇叭表示這個事項不須要提醒。服務器

預定提醒/取消提醒

咱們還會添加讓用戶對通知作出響應的功能:閉包

取消提醒

UI 部分

UI 界面上就是一個簡單的 tableView,顯示用戶的 to do list。沒什麼可說的。app

提醒事項的數據類型是這樣定義的:

class NagMeTableViewController: UITableViewController {
  typealias Task = String

  let tasks: [Task] = [
      "Wash Up",
      "Walk Dog",
      "Exercise"
  ]
  // 待續複製代碼

咱們的 tableView 就是一個提醒事項的列表,點擊 cell 上的小喇叭按鈕會調用一個閉包。

// 續上
  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "TaskCell", for: indexPath) as! TaskCell

    let task = tasks[indexPath.row]

    cell.nameLabel.text = task

    // 顯示 cell 上提醒/不提醒的圖標
    retrieveNotification(for: task) {
        request in
        request != nil ? cell.showReminderOnIcon() : cell.showReminderOffIcon()
    }

    // 點擊按鈕時調用閉包
    cell.onButtonSelection = {
        [unowned self] in
        self.toggleReminder(for: task)
    }

    return cell
  }

  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
      return tasks.count
  }
}複製代碼

爲了判斷用戶是否是當前『正在被提醒』,咱們要調一個 retrieveNotification(for: task) 方法,待會再詳細說。若是存在 notification 對象,說明用戶要求提醒這個事項。

當點擊 cell 上喇叭按鈕的時候,會調用一個 toggleReminder(for: task) 方法,咱們也放在後文介紹。這個方法裏就是預定提醒的神奇魔法。

請求用戶受權

在預定提醒以前,須要先向用戶請求通知的受權。在 app 啓動時調用以下代碼:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
  UNUserNotificationCenter.current().requestAuthorization(options: [.sound, .alert]) {
    granted, error in
    if granted {
      print("Approval granted to send notifications")
    }
  }
}複製代碼

調用的結果是會顯示一個彈窗,詢問用戶是否容許咱們的 app 發送通知。閉包的 granted 參數表示咱們是否取到了權限。這個彈窗只會顯示一次,不過以後用戶也能夠在設置裏進行更改。

請求受權

你會發現,User Notification框架大量的 API 使用了 completion block。這是由於向 UNUserNotificationCenter 發出的請求大部分都是在後臺線程上異步執行的。調用 current() 方法會讓框架返回一個供咱們 app 使用的 notification center 單例對象,而咱們全部的預定通知、取消通知都要經過這個單例對象來實現。

建立通知

建立、添加通知的過程實在有些冗長,咱們把代碼分解成幾部分,一步一步來看:

/// 爲 task 建立一個 notification,每分鐘重複一次
func createReminderNotification(for task: Task) {
  // 配置 notification 的 content
  let content = UNMutableNotificationContent()
  content.title = "Task Reminder"
  content.body = "\(task)!!"
  content.sound = UNNotificationSound.default()
  content.categoryIdentifier = Identifiers.reminderCategory複製代碼

咱們使用一個 UNMutableNotificationContent 對象來配置 notification 的外觀和內容。設好 title 和 content,這是後面用戶在通知 banner 裏看到的標題和內容。另外,咱們指定了通知出現時播放的聲音爲默認聲音。固然你也能夠指定一個本身想要的聲音。

最後,咱們設置 categoryIdentifier,待會爲通知添加自定義操做的時候會用到。

// 咱們但願能每 60 秒提醒咱們一次 (這也是蘋果容許的最小通知間隔)
  let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 60, repeats: true)複製代碼

通知中心會根據這個 trigger 來決定何時展現通知。若是沒提供 trigger,通知就會當即發出去。

有幾種不一樣的 trigger:

  • UNTimeIntervalNotificationTrigger : 能讓通知在一段指定長度的時間間隔後發出。若是須要,後面能夠按這個時間間隔週期性重複通知。
  • UNCalendarNotificationTrigger : 在特定的時刻進行通知,例如:早上 8 點通知。也能夠週期重複。
  • UNLocationNotificationTrigger : 在用戶進入/離開某個地點的時候進行通知。

對咱們目前的需求而言,咱們選擇 UNTimeIntervalNotificationTrigger,設定爲每分鐘重複一次。

let identifier = "\(task)"複製代碼

咱們的 app 能讓用戶爲 tasks 數組裏的每一項 task 添加通知。而這個 identifier 能讓咱們(沒錯,你猜對了)肯定跟通知相關聯的是哪一項 task。

// 用上面寫好的部分來組建一個 request 
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)複製代碼

使用上面講過的 identifier、content、trigger,咱們建立了一個 UNNotificationRequest 對象,它含有通知所需的全部信息。咱們再把這個對象傳給通知中心:

UNUserNotificationCenter.current().add(request) {
    error in
    if let error = error {
      print("Problem adding notification: \(error.localizedDescription)")
    }
    else {
      // 設置喇叭圖標
      DispatchQueue.main.async {
        if let cell = self.cell(for: task) {
            cell.showReminderOnIcon()
        }
      }
    }
  }
}複製代碼

若是添加通知沒有問題,咱們就更新那個 task 對應的 cell 上顯示的喇叭圖標,表示提醒已經打開了。注意 UI 操做須要回到主線程來進行,這是由於添加通知的 completion block 是在後臺線程上調用的。

取消通知

上面提到過,咱們寫了一個 retrieveNotification 方法來取消以前預定的通知。使用新的通知 API 實現這個功能很是簡單:

func retrieveNotification(for task: Task, completion: @escaping (UNNotificationRequest?) -> ()) {
  UNUserNotificationCenter.current().getPendingNotificationRequests {
    requests in
    DispatchQueue.main.async {
      let request = requests.filter { $0.identifier == task }.first
      completion(request)
    }
  }
}複製代碼

爲了照顧到以前寫的 completion block,咱們要把回調切回主線程。

把通知操做與界面關聯起來

前面配置 tableViewCell 的時候,用過一個 toggleReminder 方法,來爲點擊的 task 添加或移除通知提醒。下面咱們實現這個方法:

func toggleReminder(for task: Task) {
  retrieveNotification(for: task) {
    request in
    guard request != nil else {
        // 以前並無通知,因此該添加通知
        self.createReminderNotification(for: task)
        return
    }

    // 移除通知
    UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [task])

    // 咱們已經把通知取消了,下面更新 cell 上的喇叭圖標來顯示這一點
    if let cell = self.cell(for: task) {
        cell.showReminderOffIcon()
    }
  }
}複製代碼

若是 request 是 nil,說明以前沒有設置通知,所以咱們就設置一個。不然,就把 task 的 identifier (例如 「鍛鍊」或者「遛狗」)傳給通知中心,移除以前的通知;以後更新 cell 上的喇叭圖標,表示通知已經被禁了。

大功告成!如今咱們有了一個每 60 秒提醒一次的通知,直到用戶回到 app 裏、找到對應的 task ,把提醒關掉纔會中止。

然而,若是用戶能在通知彈出時直接關掉後續的提醒,就更好了……

添加通知的操做

咱們能夠給通知添加操做來實現這個功能。用戶在通知的 banner 下劃,或者在鎖屏界面的通知上左劃,都能看到能夠點擊的 action 按鈕。

最多能夠增長 4 種操做(雖然蘋果表示在某些設備上只能顯示前兩種操做,由於屏幕空間過小),一種操做就是一個「category」。

func addCategory() {
  // 添加操做
  let cancelAction = UNNotificationAction(identifier: Identifiers.cancelAction,
                                          title: "Cancel",
                                          options: [.foreground])

  // 建立 category
  let category = UNNotificationCategory(identifier: Identifiers.reminderCategory,
                                        actions: [cancelAction],
                                        intentIdentifiers: [],
                                        options: [])

  UNUserNotificationCenter.current().setNotificationCategories([category])
}複製代碼

咱們把 action 的選項設置爲UNNotificationActionOptions.foreground,意思是點擊 action 按鈕時會把應用打開到前臺。其餘可用的選項包括能夠表示這項操做要謹慎進行(例如刪除類操做),或者在執行前要先解鎖。咱們在 application(_:didFinishLaunchingWithOptions:) 裏調用 addCategory() 方法。

如今 identifier 只是簡單的字符串,一旦拼錯幾個字母就無法正常工做了。我曾經一邊寫成了 "cancel"、另外一邊寫成了 "Cancel",花了好一下子才排查出來。因此我以爲應該寫一個簡單的結構體,安放全部 identifier。

struct Identifiers {
    static let reminderCategory = "reminder"
    static let cancelAction = "cancel"
}複製代碼

爲了處理通知 banner 被點擊的事件,咱們須要實現 UNUserNotificationCenterDelegate 接口。爲簡潔起見,咱們就讓 AppDelegate 來當處理事件的 delegate,在 application(_:didFinishLaunchingWithOptions:) : 裏設置:

UNUserNotificationCenter.current().delegate = self複製代碼

而後咱們來實現點擊事件:

public func userNotificationCenter(_ center: UNUserNotificationCenter,
                                       didReceive response: UNNotificationResponse,
                                       withCompletionHandler completionHandler: @escaping () -> Void) {
  if response.actionIdentifier == Identifiers.cancelAction {
      let request = response.notification.request
      print("Removing item with identifier \(request.identifier)")
      UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [request.identifier])
  }

  completionHandler()
}複製代碼

首先判斷這個方法的調用來源是用戶點擊了通知上的 action 按鈕(也多是用戶直接點擊通知調用的,這種狀況下咱們不進行任何處理)。若是是,那麼咱們就直接移除 identifier 對應的通知。

最後調用 completionHandler 來通知系統咱們已經處理完成,它能夠該幹什麼幹什麼去了。

Action_Cancel.gif

好,咱們快說完了。可是若是咱們的 app 正在前臺的時候,通知就來了,會怎麼辦呢?若是不作任何處理的話,通知就會被系統默認丟棄了。咱們簡單改一下吧。

當 app 在前臺時接收通知

這是 iOS 10 新加的一個頗有用的功能:你能夠選擇當 app 在前臺時是否顯示通知。只需實現 delegate 方法,添加一句代碼:

func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification,
                                withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
  completionHandler(.alert)
}複製代碼

上面的寫法就是告訴系統,應該用 alert 顯示通知。

擴展閱讀

本文介紹了新的 UserNotifications 框架在預定本地通知方面的強大功能。看起來蘋果終於聽取了開發者的抱怨,推出了可讀易用的 API。

雖然咱們沒有篇幅詳細探討遠程推送的通知,新的框架在這方面也有所改進,它讓本地和遠程推送的通知能用相同的 API 統一處理,所以減小了代碼冗餘。

要了解更多,能夠觀看 WWDC 2016 的視頻 Introduction to Notifications。同時,歡迎來戳咱們在 Github 上的樣例工程。

原文地址:iOS 10 Day by Day :: Day 5 :: User Notifications

原做者:Sam Burnstone @sam_burnstone

ShinobiControls 官網:ShinobiControls.com twitter : @shinobicontrols

文集地址:iOS 10 day by day 倉薯翻譯

譯者:戴倉薯

相關文章
相關標籤/搜索