WWDC2016 Session筆記 - iOS 10  推送Notification新特性

一縷殤流化隱半邊冰霜發表於2016-07-24

前言

在今年6月14號蘋果WWDC開發者大會上,蘋果帶來了新的iOS系統——iOS 10。蘋果為iOS 10帶來了十大項更新。蘋果高階副總裁Craig Federighi稱此次對iOS的更新是“蘋果史上最大的iOS更新”。

  1. 新的螢幕通知檢視方式:蘋果為iOS 10帶來了全新的通知檢視功能,即抬起iPhone的螢幕,使用者就能看到目前的通知和更新情況。
  2. 蘋果將Siri開放給第三方開發者: 現在使用者可以讓Siri實現更多的功能,例如讓Siri向自己的聯絡人傳送微信資訊等。目前Siri可以直接支援的應用有微信、WhatsApp以及Uber、滴滴、Skype等。
  3. Siri將會更加智慧:Siri將擁有更多對語境的意識。基於使用者的地點、日曆、聯絡人、聯絡地址等,Siri會做出智慧建議。Siri將越來越成為一個人工智慧機器人,具備深度學習功能。
  4. 照片應用更新:基於深度學習技術,iOS 10對照片應用有比較大的更新。iOS 10對照片的搜尋能力進一步增強,可以檢測到新的人物和景色。未來的iPhone能夠將相關的照片組織在一起,比如某次旅行的照片、某個週末的照片,並且能夠進行自動編輯。iOS 10照片還新增了一個“記憶”標籤。
  5. 蘋果地圖:有點類似Siri和照片的更新,蘋果地圖也增加了很多預測功能,例如蘋果地圖能夠將提供附近的餐廳建議。蘋果地圖的介面也得到了重新設計,更加的簡潔,並增加了交通實時資訊。新的蘋果地圖還將整合在蘋果CarPlay中,將為使用者提供turn-by-turn導航功能。和Siri一樣,地圖也將開放給開發者。
  6. 蘋果音樂:蘋果音樂的介面得到了更新,介面會更加簡潔、支援多工,增加最近播放列表。蘋果音樂現在已經有1500萬付費使用者。
  7. HomeKit:iOS 10新增智慧家庭應用,支援一鍵場景模式,HomeKit可以與Siri相連線。

  8. 蘋果電話:蘋果更新了電話功能,來電時可以區別出騷擾電話。

  9. iMesseage:在iMessage方面,使用者可以直接在文字框內傳送視訊、連結,分享實時照片。另外,蘋果還增添了表情預測功能,打出的文字若和表情相符,將會直接推薦相關表情。

以下是我關於關於iOS 10中變化比較大的推送通知的學習筆記。

目錄

  • 1.Notification User Interface
  • 2.Media Attachments
  • 3.Customize user interface
  • 4.Customize Actions

一. Notification User Interface

讓我們先來看看使用者推送在iOS X中的樣子,如下圖

上圖這是在鎖屏介面下的推送。支援抬起手機喚醒功能。

上圖是Banner,可以看到這個推送更加的易讀,並且包含更多的內容。

上圖是通知中心。從上面三種圖可以看到,它們都長一個樣。

在iOS 8 中,我們可以給推送增加使用者操作,這樣使推送更加具有互動性,並且允許使用者去處理使用者推送更加的迅速。到了iOS 9 中,蘋果又再次增加了快速回復功能,進一步的提高了通知的響應性。開發者可以允許使用者通過點選推送,並用文字進行回覆。再就到了iOS 10 中,推送變得更加給力。因為在iOS X中,推送對iOS系統來說,是很重要的一部分。在日常使用中,我們會經常和推送打交道。推送是我們和裝置進行互動非常重要的方式。

在iOS X 中,你可以按壓推送,推送就會被展開,展示出更加詳細的使用者介面。展示出來的詳細介面對使用者來說,提供了更加有用的資訊。使用者可以通過點選下面的按鈕,來處理一些事件,並且推送的詳細介面也會跟著使用者的操作進行更新UI介面。

iOS 8 中iMessage支援了快速回復功能,但是你只能看見一條資訊,並且你也只能回覆一條資訊。但是在iOS X中,你可以展開推送,這個時候你就可以看到整個對話的內容了。你可以等待你的朋友回覆,你再回復他,並且可以回覆很多條。

以上就是iOS X的強大功能。以上的所有功能都能通過iOS X的新API來實現。所有的新特性都能在我們開發者開發的app裡面有所體現。

二. Media Attachments

如果經常使用iMessage的朋友們,就會經常收到一些資訊,附帶了一些照片或者視訊,所以推送中能附帶這些多媒體是非常重要的。如果推送中包含了這些多媒體資訊,可以使使用者不用開啟app,不用下載就可以快速瀏覽到內容。眾所周知,推送通知中帶了push payload,及時去年蘋果已經把payload的size提升到了4k bites,但是這麼小的容量也無法使使用者能傳送一張高清的圖片,甚至把這張圖的縮圖包含在推送通知裡面,也不一定放的下去。在iOS X中,我們可以使用新特性來解決這個問題。我們可以通過新的service extensions來解決這個問題。

為了能去下載service extension 裡面的attachment,我們必須去按照如下的要求去設定你的推送通知,使你的推送通知是動態可變的。


{
    aps: {
        alert : {……}
        mutable-content : 1
    }
    my-attachment : https://example.com/phtos.jpg"
}複製程式碼

在上面程式碼中,可以看到載入了一個mutable-content 的flag,然後我們就可以引用一個連結,把你想加入到推送裡面的attachments加入到裡面來。在上面的例子裡面,我們就加入了一個URL。更復雜的,你甚至可以去加入一個identifier來標示你想加入到推送裡面的內容,這個identifier是你app知道的,app能通過拿到identifier,然後知道去你自己的伺服器哪裡去下載內容。

通過設定完上述的部分,推送就被推送到了每個裝置的Service Extension那裡了。在每個裝置裡面的Service Extension裡面,就可以下載任意想要的attachment了。然後推送就會帶著下載好的attachment推送到手機並顯示出來了。

如果來設定Service Extension呢?來看看如下的程式碼:


// Adding an attachment to a user notification

public class NotificationService: UNNotificationServiceExtension {
    override public func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: (UNNotificationContent) -> Void)
    {
        let fileURL = // ...
        let attachment = UNNotificationAttachment(identifier: "image",
                                                  url: fileURL,
                                                  options: nil)
        let content = request.content.mutableCopy as! UNMutableNotificationContent 
        content.attachments = [ attachment ]
        contentHandler(content)
    }
}複製程式碼

首先定義了一個didReceive的方法,用來接收request,後面跟著withContentHandler的回撥函式。 這個NotificationServiceExtension會在收到推送之後,被呼叫,然後在這個方法裡面去下載自己的attachment。下載可以通過URL,或者任何你喜歡的方式。當下載完成之後,就可以建立attachment物件了。建立完UNMutableNotificationContent,我們就可以把這個加入到推送的content中了。最後,通過contentHandler回撥,把它傳遞給iOS系統,iOS 系統就會展示給使用者。

通過以上的設定,我們就能在推送中看到豐富的媒體資訊了。使用者並不需要去開啟app,也不用去點選下載。

簡單的概述一下Media Attachments:

  1. 新特性使推送支援附帶Media Attachments。本地推送和遠端推送同時都可支援。
  2. attachment支援圖片,音訊,視訊,系統會自動提供一套可自定義化的UI,專門針對這3種內容。
  3. 在service extension裡面去下載attachment,但是需要注意,service extension會限制下載的時間,並且下載的檔案大小也會同樣被限制。這裡畢竟是一個推送,而不是把所有的內容都推送給使用者。所以你應該去推送一些縮小比例之後的版本。比如圖片,推送裡面附帶縮圖,當使用者開啟app之後,再去下載完整的高清圖。視訊就附帶視訊的關鍵幀或者開頭的幾秒,當使用者開啟app之後再去下載完整視訊。
  4. 把下載完成的attachment加入到notification中。
  5. 推送裡面包含的attachment這些檔案,是由系統幫你管理的,系統會把這些檔案放在單獨的一個地方,然後統一管理。
  6. 額外說明一點,推送的attachment的圖片還可以包含GIF圖。

通過以上可以看出,Media Attachments非常的酷,它為我們提供了更加豐富的推送內容。

接下來我們再來看看如何自定義推送的使用者介面

三. Customize user interface

要想建立一個自定義的使用者介面,需要用到Notification content extension。

先來說說下面這個例子的應用場景:

比如有個朋友在日曆中給我了一個聚會的邀請,這個時候就來了推送,推送裡面的內容就是包含了聚會的時間地點資訊,推送下面有三個按鈕,接受,謝絕。下面的例子都以此為例。

Notification content extension允許開發者加入自定義的介面,在這個介面裡面,你可以繪製任何你想要的東西。但是有一個最重要的限制就是,這個自定義的介面沒有互動。它們不能接受點選事件,使用者並不能點選它們。但是推送通知還是可以繼續與使用者進行互動,因為使用者可以使用notificaiton的actions。extension可以處理這些actions。

接下來我們就來說說如何自定義介面

1. 推送的四部分

先來看一個日曆的推送例子:

上圖,整個推送分4段。使用者可以通過點選Header裡面的icon來開啟app,點選取消來取消顯示推送。Header的UI是系統提供的一套標準的UI。這套UI會提供給所有的推送通知。

Header下面是自定義內容,這裡就是顯示的Notification content extension。在這裡,就可以顯示任何你想繪製的內容了。你可以展示任何額外的有用的資訊給使用者。

content extension下面就是default content。這裡是系統的介面。這裡的系統介面就是上面推送裡面payload裡面附帶的內容。這也就是iOS 9 之前的推送的樣子。

最下面一段就是notification action了。在這一段,使用者可以觸發一些操作。並且這些操作還會相應的反映到上面的自定義的推送介面content extension中。

2.建立Notification content extension

接下來我們就來看看如何建立一個Notification content extension

第一件事就是去建立一個新的target。建立好了之後,Xcode會自動幫我們生成一個template。template會在新的target裡面生成3個檔案,一個新的ViewController,main Interface storyboard,info.plist。info.plist中就是可以定義化一些target的配置。

開啟Notification content extension的ViewController


// Minimal Content Extension
class NotificationViewController: UIViewController, UNNotificationContentExtension {
    @IBOutlet var label: UILabel?
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any required interface initialization here.
    }
    func didReceive(_ notification: UNNotification) {
        label?.text = notification.request.content.body
    }
}複製程式碼

我們會發現,這個ViewController是UIViewController的子類,其實就是一個很普通的ViewController,和我們平時使用的沒有啥兩樣。後面是UNNotificationContentExtension的protocol,這裡是系統要求你必須實現的協議。

UNNotificationContentExtension只有一個required的方法,就是didReceive方法。當推送到達你的裝置之後,這個didReceive方法會隨著ViewController的生命週期的方法 ,一起被呼叫。當開發者給推送加上expands的時候,一旦推送送達以後,這時會接到所有的ViewController生命週期的方法,和didReceive方法。這樣,我們就可以接收notification object ,接著更新UI。

3. 配置target

接下來,我們需要做的是,告訴iOS系統,推送送達之後,iOS系統如何找到你自定義的Notification content extension。

Notification content extension和我們註冊notification actions一樣,註冊的相同的category。這個例子中,我們使用event-invite。值得提到的一點是,這裡的extension是可以為一個陣列的,裡面可以為多個category,這樣做的目的是多個category共用同一套UI。

上圖中,event-invite 和 event-update就共用了一套UI。這樣我們就可以把他們打包到一個extension裡面來。但是不同的category是獨立的,他們可以相應不同的actions。

通過以上設定,iOS系統就知道了我們的target了。

4. 自定義使用者UI介面

接下來我們來自定義UI介面。


// Notification Content Extension
class NotificationViewController: UIViewController, UNNotificationContentExtension {
    @IBOutlet var eventTitle: UILabel!
    @IBOutlet var eventDate: UILabel!
    @IBOutlet var eventLocation: UILabel!
    @IBOutlet var eventMessage: UILabel!

    func didReceive(_ notification: UNNotification) {
        let content = notification.request.content

        eventTitle.text = content.title
        eventDate.text = content.subtitle
        eventMessage.text = content.body

        if let location = content.userInfo["location"] as? String {
            eventLocation.text = location
        }
    }
}複製程式碼

上述程式碼中,我們在stroyboard 裡面加入了一些labels 。當接收到推送的時候,我們提取出內容,得到我們想要的內容,然後把這些內容設定到label上面去,並展示出來。在content的userinfo裡面我們還能加入一些額外的資訊,這些資訊是標準的payload無法展示的,比如說位置資訊等等。

程式碼完成之後就是如上的樣子,中間就是我們自定義的UIView了。但是這樣子會有2個問題。第一個問題就是這個自定義的View實在太大了。大量的空白不需要顯示出來。第二個問題就是我們自定義的內容和下面預設的推送內容重複了。我們需要去掉一份。

5.改進

我們先來改進上面說的第二個問題。 這個問題很簡單,其實就是一個plist的設定。我們可以在plist裡面把預設的content隱藏。設定如下圖。

再來說說第一個問題,介面大小的問題。 我們可以通過平時我們Resize其他ViewController一樣,來Resize這個ViewController。來看看如下的程式碼。


// Notification Content Extension
class NotificationViewController: UIViewController, UNNotificationContentExtension {
    override func viewDidLoad() {

        super.viewDidLoad()
        let size = view.bounds.size

        preferredContentSize = CGSize(width: size.width, height: size.width / 2)
    }

    func didReceive(_ notification: UNNotification) {
        // ...
    }
}複製程式碼

這裡我們也可以加入constraints來做autolayout。

解決完上面2個問題,介面就會變成這個樣子。看上去比之前好很多了。正常的尺寸,沒有多餘的空白。沒有重複資訊。但是這又出現了另外一個問題。當通知展示出來之後,它的大小並不是正常的我們想要的尺寸。iOS系統會去做一個動畫來Resize它的大小。如下圖,系統會先展現出第一張圖,然後緊接著展示第二張圖,這個使用者體驗很差。

會出現上面這張圖的原因是,在推送送達的那一刻,iOS系統需要知道我們推送介面的最終大小。但是我們自定義的extension在系統打算展示推送通知的那一刻,並還沒有啟動。所以這個時候,在我們程式碼都還沒有跑起來之前,我們需要告訴iOS系統,我們的View最終要展示的大小。

現在問題又來了。這些通知會跑在不同的裝置上,不同的裝置的螢幕尺寸不同。為了解決這個問題,我們需要設定一個content size ratio。

這個屬性定義了寬和高的比例。當然設定了這個比例以後,也並不是萬能的。因為你並不知道你會接受到多長的content。當你僅僅只設定比例,還是不能完整的展示所有的內容。有些時候如果我們可以知道最終的尺寸,那麼我們固定尺寸會更好。

6. 進一步美化

我們可以給這個extension加上Media Attachments。一旦我們加入Media Attachments,我們可以在content extension裡面使用這些內容。


// Notification Content Extension Attachments
class NotificationViewController: UIViewController, UNNotificationContentExtension {
    @IBOutlet var eventImage: UIImageView!
    func didReceive(_ notification: UNNotification) {
        let content = notification.request.content
        if let attachment = content.attachments.first {
            if attachment.url.startAccessingSecurityScopedResource() {
                eventImage.image = UIImage(contentsOfFile: attachment.url.path!)
                attachment.url.stopAccessingSecurityScopedResource()
            }
        }
    }
}複製程式碼

我們可以提取content的attachments。前文提到過,attachment是由系統管理的,系統會把它們單獨的管理,這意味著它們儲存在我們sandbox之外。所以這裡我們要使用attachment之前,我們需要告訴iOS系統,我們需要使用它,並且在使用完畢之後告訴系統我們使用完畢了。對應上述程式碼就是startAccessingSecurityScopedResource()和stopAccessingSecurityScopedResource()的操作。當我們獲取到了attachment的使用權之後,我們就可以使用那個檔案獲取我們想要的資訊了。

上述例子中,我們從attachment中獲取到圖片,並展示到UIImageView中。於是notification就變成下面這個樣子了。

四.Customize Actions

說道這裡,我們不得不說一下iOS8開始引入的action的工作原理: 預設系統的Action的處理是,當使用者點選的按鈕,就把action傳遞給app,與此同時,推送通知會立即消失。這種做法很方便。

但是還有一種情況,當使用者點選了按鈕,希望接受一些日曆上的邀請,我們需要把這個操作即時的展示在我們自定義的UI上,這是我們就只能用Notification content extension來處理這些使用者點選事件了。這個時候,使用者點選完按鈕,我們把這個action直接傳遞給extension,而不是傳遞給app。當actions傳遞給extension時,它可以延遲推送通知的消失時間。在這段延遲的時間之內,我們就可以處理使用者點選按鈕的事件了,並且更新UI,一切都處理完成之後,我們再去讓推送通知消失掉。

這裡我們可以運用UNNotificationContentExtension協議的第二個方法,這方法是Optional


// Intercepting notification action response
class NotificationViewController: UIViewController, UNNotificationContentExtension {
    func didReceive(_ response: UNNotificationResponse, completionHandler done: (UNNotificationContentExtensionResponseOption) -> Void) {
        server.postEventResponse(response.actionIdentifier) {
            if response.actionIdentifier == "accept" {
                eventResponse.text = "Going!"
                eventResponse.textColor = UIColor.green()
            } else if response.actionIdentifier == "decline" {
                eventResponse.text = "Not going :("
                eventResponse.textColor = UIColor.red()
            }
            done(.dismiss)
        }
    }
}複製程式碼

不用這個方法的時候就可以不宣告出來。但是一旦宣告瞭,那麼你就需要在這個方法裡面處理推送通知裡面所有的actions。這就意味著你不能只處理一個action,而不管其他的action。

在上述程式碼中,當使用者點選了按鈕,這個時候我們同步一下伺服器資訊,當接收到了伺服器應答之後,然後我們更新UI。使用者點選了“accept”之後,表示接受了這次聚會邀請,於是我們把text的顏色變成綠色。當使用者點選了“decline”,表示謝絕,於是我們把text的顏色變成紅色。當使用者點選之後,更新完介面,我們就讓推送通知消失掉。

這裡值得一提的是,如果你還想把這個action傳遞給app,那麼最後的引數應該是這樣。

done(.dismissAndForwardAction)複製程式碼

引數設定成這樣之後,使用者的action就會再傳遞給app。

如果此時使用者還想輸入寫文字來評論這條推送,我們該如何做?

這個輸入文字的需求是來自於iOS 9 。這個的使用方法和9是相同的。


// Text Input Action
private func makeEventExtensionCategory() -> UNNotificationCategory {
    let commentAction = UNTextInputNotificationAction(
        identifier: "comment",
        title: "Comment",
        options: [],
        textInputButtonTitle: "Send",
        textInputPlaceholder: "Type here...")
    return UNNotificationCategory(identifier: "event-invite", actions: [ acceptAction, declineAction, commentAction ],
}複製程式碼

我們可以建立一個UNTextInputNotificationAction,並把它設定到plist裡面的Category中。當推送通知到來之後,使用者點選了按鈕,textfield就會顯示出來。同樣的處理action程式碼如下:


// Text input action response
class NotificationViewController: UIViewController, UNNotificationContentExtension {
    func didReceive(_ response: UNNotificationResponse,
                      completionHandler done: (UNNotificationContentExtensionResponseOption) -> Void) {
        if let textResponse = response as? UNTextInputNotificationResponse {
            server.send(textResponse.userText) {
            }
        }
    }
}複製程式碼

這個時候當使用者點選了評論按鈕,就會彈出textfield。

這裡還有一個問題,就是使用者點完評論按鈕之後,之前的接受和謝絕的按鈕就消失了。這個時候使用者可能有這個需求,想又評論,又接受或者謝絕。那麼我們就需要在下面鍵盤上加入這兩個按鈕。如下圖這樣子。

這裡並沒有新的API,還是用原來的API。我們可以使用已經存在的UIKit的API去定製輸入的input accessory view。它可以讓我們開發者加入自定義的按鈕。


// Custom input accessory view
class NotificationViewController: UIViewController, UNNotificationContentExtension {
    override func canBecomeFirstResponder() -> Bool {
        return true
    }
    override var inputAccessoryView: UIView { get {
        return inputView
        }
    }
    func didReceive(_ response: UNNotificationResponse,
                      completionHandler done: (UNNotificationContentExtensionResponseOption) -> Void) {
        if response.actionIdentifier == "comment" {
            becomeFirstResponder()
            textField.becomeFirstResponder()
        }
    }
}複製程式碼

解析一下上述的程式碼。首先我們需要讓ViewController BecomeFirstResponder。這裡做了2件事情,一是告訴responder chain,我成為了第一響應者,二是告訴iOS系統,我不想使用系統標準的text field。接著就可以建立自定義化的inputAccessoryView。如上圖中顯示的,帶自定義的兩個按鈕。然後,當extension接受到了使用者點選按鈕後產生的action,這時自定義的textfield就會變成第一響應者,並且伴隨著鍵盤的彈起。

注意,這裡需要2個becomeFirstResponder,第一個becomeFirstResponder是使viewController變成第一響應者,這樣textfield就會出現。第二個becomeFirstResponder是使我們自定義的textfield變成第一響應者,這樣鍵盤才會彈起。

總結

以上就是iOS X中notification的所有新特性,通過上文,我們學到的以下的知識,總結一下:

  1. 什麼是attachment
  2. 如何在service extension中使用attachment
  3. 如何定義content extension的使用者UI介面
  4. 如何響應使用者操作action

最後,請大家多多指教。新浪微博@halfrost。

相關文章