Swift 程式語言

結合 iOS 10 的 User Notifications:傳送米花兒的幸福打氣通知

結合 iOS 10 的 User Notifications:傳送米花兒的幸福打氣通知
結合 iOS 10 的 User Notifications:傳送米花兒的幸福打氣通知
In: Swift 程式語言

通知在 iOS 是個讓人又愛又恨的功能。因為通知,我們按三餐收到情人的甜言蜜語。也因為通知,害我們凌晨三點收到情敵的恐怖訊息。接下來就讓我們結合 iOS 10 最新的 UserNotifications framework,實現散播幸福散播愛的米花兒通知。

user-notification-0

註:特別感謝插畫家 Hana 提供米花兒的插畫和文字。

徵求使用者同意,獲得發送通知的權限

收到心上人的通知很開心,收到不是人的通知卻很恐怖。Apple 特別重視別讓使用者不開心,唯有徵求使用者同意後,App 才擁有發送通知的權力。明人不做暗事,我們就在 App 啟動時徵求使用者同意吧。

編者註:首先,下載這個範本項目,然後跟著以下步驟學習加入通知(User notifications)。另外,請記得使用Xcode 8打開範本項目。

加入 UserNotifications Framework

import UserNotifications

在 iOS 10,通知相關功能定義在 UserNotifications framework ,所以我們必須先將它 import。請將以上程式碼加進AppDelegate.swift

徵求使用者同意

之後,修改 AppDelegateapplication(_:didFinishLaunchingWithOptions:),詢問使用者是否願意收到來自米花兒的幸福通知:

 
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge], completionHandler: { granted, error in
            if granted {
                print("使用者同意了,每天都能收到來自米花兒的幸福訊息")
            }
            else {
                print("使用者不同意,不喜歡米花兒,哭哭!")
            }
            
        })

        return true
}

UNUserNotificationCenter 物件是通知中心,幫我們打點推知相關的大小事。我們利用 UNUserNotificationCenter.current() 取得 UNUserNotificationCenter 物件,然後再呼叫它的 requestAuthorization(options:completionHandler:),徵求使用者同意 App 發送通知。此 function 的宣告如下:

open func requestAuthorization(options: UNAuthorizationOptions = [], completionHandler: @escaping (Bool, Error?) -> Swift.Void)

參數說明:

options:設定我們希望使用者同意的通知樣式。它的型別為 UNAuthorizationOptions,定義如下:

public struct UNAuthorizationOptions : OptionSet {

    public init(rawValue: UInt)

    public static var badge: UNAuthorizationOptions { get }
    public static var sound: UNAuthorizationOptions { get }
    public static var alert: UNAuthorizationOptions { get }
    public static var carPlay: UNAuthorizationOptions { get }
}

米花兒的幸福通知同時包含感人的圖文(alert),好聽的聲音(sound),以及在 App Icon 顯示數字(badge),提醒使用者有新的通知。因此我們傳入 [.alert, .sound, .badge]

completionHandler: 在使用者開心同意或狠心拒絕我們時執行,我們傳入 closure,將 closure 的參數取名為 grantederror。Bool 型別的 granted 告訴我們使用者是否同意,若有錯誤則可從 Error 型別的 error 了解錯誤原因。

試一試執行程式

打開 App 後,迫不及待地跳出徵求同意的詢問訊息。

user-notification-1

值得注意的,詢問訊息只會出現一次。當我們下次啟動 App 時,再也看不到它。如果使用者不小心按到 Don’t Allow,App 不能再死纏爛打地跳出詢問訊息,就好像真實世界裡,跟心儀對象表白被拒絕後,就應該安靜地走開一樣。不過使用者如果突然良心發現,想要同意接收 App 的通知倒是可以的,只要他從設定 App 進入 App 的通知設定頁面,打開 Allow Notifications 的開關。

user-notification-2

註:有一種特別的情況我們可以再次看到通知的詢問訊息。當 App 被整個移除,重新安裝後,我們將再次重溫詢問訊息美麗的容顏。

發送散播幸福散播愛的米花兒通知

首先,在 ViewController.swift 加入 UserNotifications framework。

 import UserNotifications

之後,設定點擊按鈕「散播米花兒的祝福通知」,觸發 ViewController 的 createNotification(_:)

user-notification-3

編者註:如你使用我們提供的範本程式,「散播米花兒的祝福通知」按鈕已連結到createNotification方法。

createNotification(_:)更新如下:

@IBAction func createNotification(_ sender: AnyObject) {
        
        let content = UNMutableNotificationContent()
        content.title = "體驗過了,才是你的。"
        content.subtitle = "米花兒"
        content.body = "不要追問為什麼,就笨拙地走入未知。感受眼前的怦然與顫抖,聽聽左邊的碎裂和跳動。不管好的壞的,只有體驗過了,才是你的。"
        content.badge = 1
        content.sound = UNNotificationSound.default()
        
        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 10, repeats: false)
        let request = UNNotificationRequest(identifier: "notification1", content: content, trigger: trigger)
        UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
}

先試一試,之後再解說

要測試的話,先按「散播米花兒的祝福通知」按鈕,然後跳到Home畫面(command+shift+h)或Lock(command+L)畫面。如無意外,你會見到米花兒的祝福通知。

user-notification-4

通知內容的 body 欄位文字太多,變成 … 了,怎麼辦呢 ? 別擔心,只要動動你的小指,將通知往下拖曳,即可展開顯示完整的內容。

user-notification-5

createNotification 細部解說

好!現在就讓我們看看createNotification的内容:

(1) 建立 UNMutableNotificationContent 物件 content,設定感人肺腑的通知內容。

(2) 設定 title,subtitle 和 body 欄位,對應通知顯示的文字內容。值得注意的,想看到通知,一定要設定 body,只設定 title 和 subtitle 的話,就算你再等一百年也看不到它。

(3) badge 欄位設定 App Icon 顯示的數字。

(4) sound 欄位設定通知發出的聲音,型別為 UNNotificationSound。若無指定將成為無聲通知。為了提醒使用者米花兒幸福通知的來到,我們將聲音指定為 UNNotificationSound.default(),發出預設的通知聲。倘若想指定聲音也很簡單,只要先將音檔加到專案裡,然後指定檔名即可建立 UNNotificationSound 物件,例如以下例子。( 不過音檔這部分比較龜毛,一定得是 aiff,wav 或 caf,而且長度不能超過 30 秒。)

content.sound = UNNotificationSound(named: "小幸運")

(5) 建立 UNTimeIntervalNotificationTrigger 物件 trigger,設定通知觸發的條件。經由 UNTimeIntervalNotificationTrigger 物件,我們可設定幾秒鐘之後觸發通知,參數 timeInterval 設定秒數,repeats 設定是否重覆。

如果不喜歡對方,想吵到對方發瘋,可以試試將 timeInterval 設 10,repeats 設 true,每 10 秒鐘生成一則通知。

let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 10, repeats: true)

可惜,Apple 早已想到這點,怕有人心懷不軌。當我們設定小於 60 秒觸發的重覆通知時,馬上會得到報應,App 立即閃退 ! Console 顯示以下錯誤訊息 :

2016-08-16 17:20:53.713 NotificationDemo[14959:707417] *** Terminating app due to uncaught exception NSInternalInconsistencyException, reason: 'time interval must be at least 60 if repeating'

其實總共有四種方法可以觸發通知,UNTimeIntervalNotificationTrigger 只不過是其中一種,有興趣的朋友可進一步研究以下幾種不同的方法 :

  1. UNTimeIntervalNotificationTrigger: 幾秒鐘後觸發。

  2. UNCalendarNotificationTrigger: 指定某個時刻觸發。(相關程式碼連結)

  3. UNLocationNotificationTrigger: 使用者靠近某個位置時觸發。比方當你靠近彼得潘家時,手機馬上會收到通知,提醒你找彼得潘喝下午茶。

  4. UNPushNotificationTrigger: 從千里之外的後台傳送到使用者手機的通知,比方彼得潘失戀時緊急發送給大家的討拍拍通知。

(6) 建立 UNNotificationRequest 物件 request。有了它,我們才能跟通知中心請求發送通知,其宣告如下:

public convenience init(identifier: String, content: UNNotificationContent, trigger: UNNotificationTrigger?)

建立 request 時,我們傳入剛剛生成的 content 和 trigger,如此通知中心才能明白通知的內容和觸發的條件。參數 identifier 設定 request 的 id。如果未來我們反悔,想取消原本請求的通知,則可透過此 identifier 取消 request。人生總是時常在反悔的朋友可先記一下以下幾種來自 UNUserNotificationCenter,取消 request 的方法。

open func removePendingNotificationRequests(withIdentifiers identifiers: [String])
open func removeAllPendingNotificationRequests()
open func removeDeliveredNotifications(withIdentifiers identifiers: [String])
open func removeAllDeliveredNotifications()

Pending 是使用者還沒收到的未來通知,Delivered 則是已經收到,但還顯示在通知頁面,還未點開觀看的通知,例如以下的圖片。(其實就是令人難過的已讀不回啦。)

user-notification-6

(7) 呼叫 UNUserNotificationCenter 物件的 add(_:withCompletionHandler:),傳入 request,跟通知中心請求發送通知。function 的宣告如下:

 public func add(_ request: UNNotificationRequest, withCompletionHandler completionHandler: ((Error?) -> Swift.Void)? = nil)

到時候系統將呼叫 completionHandler 參數,經由它的 Error 型別參數告訴我們通知請求是否被接受。因此我們可在 completionHandler 傳入 closure,判斷請求是否成功。不過在此我們不管結果,傳入 nil。就好像電影那些年柯騰跟沈佳宜表白時,請對方不要現在告訴他結果 :

柯騰: 拜託不要現在告訴我。請讓我,繼續喜歡妳。
– 電影那些年的經典台詞

在通知加入圖片,音樂,影片

通知裡只有文字未免單調,在這個年代,大家的胃口都被養大了,最好能搭配一些圖片影音,勾引誘惑使用者。透過 UNNotificationAttachment 物件,想加入這些內容可說是輕而易舉! 接下來就讓我們瞧瞧如何加入米花兒精心繪製的插畫圖片。

先將以下程式碼加入 createNotification 方法並插在 trigger 變數之前:

let imageURL = Bundle.main.url(forResource: "pic", withExtension: "jpg")
let attachment = try! UNNotificationAttachment(identifier: "", url: imageURL!, options: nil)
content.attachments = [attachment]

生成 UNNotificationAttachment 物件

我們先取得圖片的 url,然後再生成 UNNotificationAttachment 物件。UNNotificationAttachmentinit 宣告如下:

public convenience init(identifier: String, url URL: URL, options: [AnyHashable : Any]? = nil) throws

UNNotificationAttachment 物件的內容可以是圖片,音樂或影片,只要我們傳給它檔案的路徑 url。值得注意的,由於需要傳入 url,所以圖片一定要加到 project navigator 的清單裡,不能放在 Assets.xcassets,因為我們無法取得裡頭圖片的 url。

user-notification-7

生成 UNNotificationAttachment 物件還需要其它 2 個參數,identifier 設定物件的 id,懶得想也可以給空字串,系統會自動幫我們生成一組 id。options 參數可做一些進階的設定,有興趣的讀者可進一步查詢 Attachment Attributes 的相關說明。

設定 UNMutableNotificationContent 物件 的 attachments 屬性

UNMutableNotificationContent 物件的屬性 attachments 設定通知包含的圖片影音。它是個可包含多個 UNNotificationAttachment 物件的 array。做人不要太貪心,在此我們先傳入剛剛生成的 attachment 就好。

執行結果

米花兒的插畫,「體驗過了,才是你的」完美地在通知框框裡現身。可惜由於框框太小,只能顯示正方形的部分截圖。

user-notification-8

想看完整版嗎?很簡單,只要一樣地以小指將通知往下拖曳,即可展開顯示完整的圖片。

user-notification-9

在前景顯示通知

通知的設計是為了讓使用者在操作其它 App 或是發呆做著白日夢時,貼心提醒使用者有大事發生。(好吧,也可能只是小事。) 因此當 App 在前景時,預設是不會顯示通知訊息的。 Apple 的設計很合理,畢竟使用者正在使用 App ,沒必要畫蛇添足跳出通知。

不過有些時候就算 App 在前景,還是有顯示通知的需求。比方說,當彼得潘在 FB 的 Messenger App 跟一輩子的coding聊天時,朋友 Chilam Lin 突然很想彼得潘,傳來我好想你的訊息。如此重要的訊息當然要馬上回覆,所以 Messenger App 不敢怠惰,立即在 App 畫面的上方顯示通知內容。

user-notification-10

Messenger App 之所以能在前景顯示推播,並不是天上掉下來的,它千辛萬苦地設計製作長方形的通知圖示,將其顯示在畫面上。不過在 iOS 10 SDK,終於不用那麼辛苦了。如果只想顯示標準的通知樣式,現在只要進行以下步驟即可實現。

遵從 UNUserNotificationCenterDelegate Protocol

我們利用 extension 擴充 AppDelegate,讓它遵從 UNUserNotificationCenterDelegate 協定並定義 userNotificationCenter(_:willPresent:withCompletionHandler:)。將以下程式碼加入 AppDelegate.swift

extension AppDelegate: UNUserNotificationCenterDelegate {

    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        completionHandler([.badge, .sound, .alert])
    }
}

待會我們會把 AppDelegate 物件設為 UNUserNotificationCenter 物件的代理人,如此當 App 在前景收到通知時, UNUserNotificationCenter 物件將請代理人執行此 function,只要在 function 裡呼叫 completionHandler,即可顯示通知。completionHandler 的參數的型別為 UNNotificationPresentationOptions,其宣告如下:

public struct UNNotificationPresentationOptions : OptionSet {

    public init(rawValue: UInt)
        
    public static var badge: UNNotificationPresentationOptions { get }
    public static var sound: UNNotificationPresentationOptions { get }
    public static var alert: UNNotificationPresentationOptions { get }
}

completionHandler 的參數設定通知如何呈現,在此我們貪心地傳入 [.badge, .sound, .alert],表示我們想同時顯示通知,發出聲音,以及更新 App Icon 上的數字。

將 AppDelegate 物件設為 UNUserNotificationCenter 物件的代理人

將此行程式碼加在 application(_:didFinishLaunchingWithOptions:) 裡:

UNUserNotificationCenter.current().delegate = self

執行結果

試試發一個米花兒的祝福通知,稍等 10 秒鐘,即可在前景看到通知。

user-notification-11

馬上傳送通知

學會在前景顯示通知後,還可搭配馬上傳送通知的密技。比方當使用者點選按鈕「顯示今日祝福通知」,馬上在畫面上顯示通知。要知道 iOS App 開發者都是一秒鐘幾十萬上下,可沒那美國時間等待。此密技十分容易,只要在生成 UNNotificationRequest 物件時,trigger 參數傳入 nil 即可。

let request = UNNotificationRequest(identifier: "notification1", content: content, trigger: nil)

在通知裡包含客製化的資訊,判斷使用者點選通知。

當使用者點選通知時,App 會自動被啟動或從背景回到前景。可是有些時候我們想帶給使用者更好的體驗,比方有懼高症的虎克船長傳 LINE 訊息約彼得潘二樓陽台對決時,點開通知後, LINE App 會馬上跳到彼得潘和虎克船長的私密聊天室。想實現如此貼心的功能,App 必須能做到以下三件事 :

  1. 發送通知時,在通知裡包含客製化的資訊。

  2. 判斷使用者點選通知。

  3. 從收到的通知裡解析客製化的資訊,進行相對應的動作,比方像 LINE App 一樣,解析訊息來自虎克船長,切換到虎克船長的聊天室。

發送通知時,在通知裡包含客製化資訊

ViewController.swift,修改 createNotification(_:),設定 content 的屬性 userInfo。它的型別是 Dictionary,因此我們可在其中搭配自訂的 key ,包含任何我們想攜帶的客製化資訊。在此我們傳入和通知內容相關的米花兒粉絲團文章網址。

content.userInfo = ["link":"https://www.facebook.com/himinihana/photos/a.104501733005072.5463.100117360110176/981809495274287"]

判斷使用者點擊通知,解析通知內容

不管 App 在前景還是背景,不管 App 是生是死,不管使用者直接點擊通知還是先把通知拖曳展開後再點擊,UNUserNotificationCenterDelegate 宣告的 userNotificationCenter(_:didReceive:withCompletionHandler:) 都會被觸發。因此我們在 UNUserNotificationCenter 物件的代理人類別 AppDelegate 裡定義此 function,即可客製化使用者點擊通知後做的事情。

將程式碼加入 AppDelegate 的 extension 內:

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler:  @escaping () -> Void) {
        
        let content = response.notification.request.content
        print("title \(content.title)")
        print("userInfo \(content.userInfo)")

        completionHandler()
}

若要解析通知的內容,必須透過參數 response,從 response.notification.request.content 挖掘出我們真正關心的通知內容,UNNotificationContent 物件,客製化的資訊正包含在它的 userInfo 裡。

為通知增添客製化的按鈕

通知預設的動作只有兩種,要嘛點擊通知打開 App,要嘛關掉通知。其實通知能做的不只這些,就像歌詞唱的,「你從不知道,我想做的不只是朋友」。接下來我們將為通知加入客製化的按鈕,當使用者收到米花兒的通知時,可依自己的喜好,喜歡選擇「好感動」,不喜歡選擇「沒感覺」。

加入客製化的按鈕,設定自訂的通知類別

修改 AppDelegateapplication(_:didFinishLaunchingWithOptions:),於其中加入以下程式碼。

let likeAction = UNNotificationAction(identifier: "like", title: "好感動", options: [.foreground])
let dislikeAction = UNNotificationAction(identifier: "dislike", title: "沒感覺", options: [])
let category = UNNotificationCategory(identifier: "luckyMessage", actions: [likeAction, dislikeAction], intentIdentifiers: [], options: [])
UNUserNotificationCenter.current().setNotificationCategories([category])

讓我為以上程式進行細部解說:

(1) 生成對應到通知按鈕的 UNNotificationAction 物件 – 在此我們生成 likeActiondislikeAction,即可在通知上顯示兩個按鈕。UNNotificationActioninit 宣告如下 :

public convenience init(identifier: String, title: String, options: UNNotificationActionOptions = [])

參數說明:

identifier: 之後可經由此處設定的 ID 判斷使用者點選的按鈕。

title: 按鈕顯示的文字。

options: 設定按鈕點選後的動作。其型別為 UNNotificationActionOptions,定義如下:

public struct UNNotificationActionOptions : OptionSet {

    public init(rawValue: UInt)

    // Whether this action should require unlocking before being performed.
    public static var authenticationRequired: UNNotificationActionOptions { get }

    // Whether this action should be indicated as destructive.
    public static var destructive: UNNotificationActionOptions { get }
    
    // Whether this action should cause the application to launch in the foreground.
    public static var foreground: UNNotificationActionOptions { get }
}

likeAction 我們傳入 [.foreground],表示點選後將打開 App。而 dislikeAction 傳入 [] 則會關閉推播,不打開 App。既然不喜歡了,最好就不相見吧。

(2) 跟通知中心註冊包含客製按鈕的特別通知 – 生成 UNNotificationCategory 物件,將它設為 UNUserNotificationCenter 物件的 NotificationCategoriesUNNotificationCategory 物件定義了 App 支援的特別通知。這裡的重點在它的 identifier 和 actions。到時候 App 收到通知時,將比對通知內容的 categoryIdentifier 找尋對應的特別通知,而特別通知顯示的按鈕即由當初傳入的 actions 決定。至於另外兩個參數 intentIdentifiers 和 options 則屬較進階的設定,可暫且忽略,先傳入 [] 即可。

因此,一個 App 其實可支援多種不同按鈕的通知,只要呼叫 setNotificationCategories 傳入包含多個 UNNotificationCategory 物件的 array,每個指定不同的 id 和 actions。

設定通知內容的類別 ID

回到 ViewController.swift,修改 createNotification(_:)並加入以下程式碼:

content.categoryIdentifier = "luckyMessage"

我們設定 content 的 categoryIdentifier 為 luckyMessage,和剛剛 UNNotificationCategory 物件的 identifier 一樣,如此到時候 App 收到通知時,才知道要顯示好感動和沒感覺的按鈕。倘若沒有設定 categoryIdentifier,或是設定的 categoryIdentifier 找不到對應的 UNNotificationCategory,則會顯示只有關閉按鈕的標準通知。

判斷使用者點選通知的哪一個按鈕

要判斷使用者點選哪一個按鈕,可以修改 userNotificationCenter(_:didReceive:withCompletionHandler:),加入以下程式碼,從 response.actionIdentifier 判斷使用者點選哪一個按鈕。

print("actionIdentifier \(response.actionIdentifier)")

如果是客製化按鈕,.actionIdentifier 將為當初 UNNotificationAction 物件的 identifier。

最後測試

執行米花兒 app ,傳送一個米花兒的幸福打氣通知。當收到米花兒的通知時,你會見到兩個客製化的按鈕。

user-notification-12

通知的其它進階功能

關於通知,其實還有許多新奇有趣的功能,比方通知的讀取,刪除和更新,遠端推播 ( Push Notification ),利用 Notification Content Extension 設計通知拖曳展開後顯示的畫面,利用 Notification Service Extension 在收到通知時解密被加密的通知內容等,有興趣的讀者可參考 Apple 的官方文件以及以下兩個 WWDC16 的影片 :

關於通知或 iOS App 開發的相關技術,大家若有任何問題,可在這裡留言。也歡迎隨時聯絡彼得潘。當彼得潘回答大家的問題時,其實也在找答案的過程中精進學習,增長了自己的功力,和大家交了朋友,獲得再多錢也買不到的回報和收獲。

你可以在GitHub下載完整Xcode項目以作參考。

作者
彼得潘
彼得潘,正職作家,副業講師,深愛 Apple 相關的所有人事物。精通 Swift iOS 程式設計,平日的興趣為桌球,情歌和寫作。除了一天一顆蘋果強身,也努力保持一天研究一項 iOS SDK 技術的習慣。著作: Swift程式設計入門,App 程式設計入門-iPhone,iPad 課程。Line ID: deeplovepeterpan
評論
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。