Notification 歷史和現(xiàn)狀
iOSVersion | 新增推送特性描述 |
---|---|
iOS 3 | 引入推送通知 UIApplication 的 registerForRemoteNotificationTypes 與 UIApplicationDelegate 的 application(:didRegisterForRemoteNotificationsWithDeviceToken:),application(:didReceiveRemoteNotification:) |
iOS 4 | 引入本地通知 scheduleLocalNotification抢腐,presentLocalNotificationNow:唆缴, application(_:didReceive:) |
iOS 5 | 加入通知中心頁面 |
iOS 6 | 通知中心頁面與 iCloud 同步 |
iOS 7 | 后臺靜默推送application(_:didReceiveRemoteNotification:fetchCompletionHandle:) |
iOS 8 | 重新設計 notification 權(quán)限請求墩邀,Actionable 通知 registerUserNotificationSettings(:)粘舟,UIUserNotificationAction 與 UIUserNotificationCategory深胳,application(:handleActionWithIdentifier:forRemoteNotification:completionHandler:) 等 |
iOS 9 | Text Input action绰疤,基于 HTTP/2 的推送請求 UIUserNotificationActionBehavior,全新的 Provider API 等 |
iOS 10 | 添加新框架:UserNotifications.framework 舞终,使用 UserNotifications 類輕松操作通知內(nèi)容 |
前言
蘋果在 iOS 10 中添加了新的框架:UserNotifications.framework 峦睡,極大的富化了推送特性翎苫,讓開發(fā)者可以很方便的將推送接入項目中,也可以更大程度的自定義推送界面榨了,同時煎谍,也讓用戶可以與推送消息擁有更多的互動,那么龙屉,這篇文章我會盡量詳細的描述 iOS 10推送新特性的使用方法呐粘。
本文參考自:AppleDevelop 遠程推送官方文檔
APNS(Apple Push Notification Service)-遠程推送原理解析
iOS app大多數(shù)都是基于client/server模式開發(fā)的,client就是安裝在我們設備上的app转捕,server就是遠程服務器作岖,主要給我們的app提供數(shù)據(jù),因為也被稱為Provider五芝。那么問題來了痘儡,當App處于Terminate狀態(tài)的時候,當client與server斷開的時候枢步,client如何與server進行通信呢沉删?是的,這時候Remote Notifications很好的解決了這個囧境醉途,當客戶端和服務端斷開連接時矾瑰,蘋果通過 APNS 與client 建立長連接。蘋果所提供的一套服務稱之為Apple Push Notification service隘擎,就是我們所謂的APNs殴穴。
推送消息傳輸路徑: Provider-APNs-Client App
我們的設備聯(lián)網(wǎng)時(無論是蜂窩聯(lián)網(wǎng)還是Wi-Fi聯(lián)網(wǎng))都會與蘋果的APNs服務器建立一個長連接(persistent IP connection),當Provider推送一條通知的時候货葬,這條通知并不是直接推送給了我們的設備采幌,而是先推送到蘋果的APNs服務器上面,而蘋果的APNs服務器再通過與設備建立的長連接進而把通知推送到我們的設備上(參考圖1-1震桶,圖1-2)植榕。而當設備處于非聯(lián)網(wǎng)狀態(tài)的時候,APNs服務器會保留Provider所推送的最后一條通知尼夺,當設備轉(zhuǎn)換為連網(wǎng)狀態(tài)時尊残,APNs則把其保留的最后一條通知推送給我們的設備;如果設備長時間處于非聯(lián)網(wǎng)狀態(tài)下淤堵,那么APNs服務器為其保存的最后一條通知也會丟失寝衫。Remote Notification必須要求設備連網(wǎng)狀態(tài)下才能收到,并且太頻繁的接收遠程推送通知對設備的電池壽命是有一定的影響的拐邪。
DeviceToken 的詳細說明
當一個App注冊接收遠程通知時慰毅,系統(tǒng)會發(fā)送請求到APNs服務器,APNs服務器收到此請求會根據(jù)請求所帶的key值生成一個獨一無二的value值也就是所謂的deviceToken扎阶,而后APNs服務器會把此deviceToken包裝成一個NSData對象發(fā)送到對應請求的App上汹胃。然后App把此deviceToken發(fā)送給我們自己的服務器婶芭,就是所謂的Provider。Provider收到deviceToken以后進行儲存等相關(guān)處理着饥,以后Provider給我們的設備推送通知的時候犀农,必須包含此deviceToken。(參考圖1-3宰掉,圖1-4)
推送前期:推送證書的配置
在開始使用推送新特性前呵哨,我們必須準備三張配置證書(如果還不清楚要如何如何配置證書,可回顧前言中的相關(guān)文章)轨奄,分別是:
iOS development (ios_development.cer)證書
iOS 測試證書CertificateSigningRequest.certSigningRequest 文件
導出CSR的過程其實就是電腦向證書機構(gòu)申請憑證的過程孟害。該證書是通過電腦制作并頒發(fā)給你的電腦的。而從電腦導出的 CSR 文件就是用于證明你的電腦具有制作證書的能力的aps_development.cer 證書
通過 appID 生成的推送證書挪拟,而 App ID其實就是一個App的身份證挨务,一個App的唯一標示。在Project中稱為Bundle ID玉组,用于指明哪個項目要開啟推送通知的服務谎柄。mobileprovisioning 配置文件
描述文件描述了可由哪臺電腦,把哪個App球切,安裝到哪臺手機上面。一個描述文件的制作是需要App ID绒障、Device吨凑、Certificate這些信息的,即簡單來說:該配置文件可以用于說明哪臺電腦中的哪個 app 需要開啟推送服務户辱,并用哪臺手機作為調(diào)試工具
推送前期:在程序中的相關(guān)配置
按照路徑: target - 程序名字 - capabilities 鸵钝,打開頁面,按照 圖1-5 所示設置:
同樣的庐镐,按照 target - 程序名字 - capabilities 路徑恩商,當你相關(guān)證書都配置完全時,程序中才會出現(xiàn) pushNotifications 的按鈕必逆,打開按鈕怠堪,如 圖1-6 所示,你會發(fā)現(xiàn)程序中多出現(xiàn)了 XXX.entitlements 文件名眉,這就是你程序中的推送配置文件粟矿。
到這里,配置相關(guān)的東西都搞定了损拢,你終于可以開始在程序中碼代碼了陌粹。
推送初探:推送權(quán)限申請 與 推送基礎(chǔ)設置
權(quán)限申請是在用戶第一次啟動 app 的時候跳出來的權(quán)限申請框,請求用戶授權(quán)推送請求福压,故而自然而然我需要在 AppDelegate 的 didFinishLaunchingWithOptions 方法中請求授權(quán)掏秩。
-
推送基礎(chǔ)設置:定義一個推送設置方面的類或舞,繼承 UNUserNotificationCenterDelegate 代理,當推送成功之后蒙幻,開發(fā)者可以在通知中心代理方法中去設置 推送界面顯示之前的 UI 樣式 和 ** 推送界面提示彈出框的點擊方法 **
// 當一個通知被提交到前臺的時候映凳,該方法會被調(diào)用。// 如果你想在前臺顯示推送消息杆煞,那么你必須返回一個有內(nèi)容的數(shù)組
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {guard let notificationType = UserNotificationType(rawValue: notification.request.identifier) else { completionHandler([]) return } let option: UNNotificationPresentationOptions switch notificationType { case .normalNotification: option = [.alert,.sound] default: option = [] } completionHandler(option) } // 第二個方法嚴格來說只要用戶點擊消息推送彈出框就會調(diào)用魏宽。 func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { print(response.actionIdentifier) completionHandler() }
-
申請授權(quán):
淺談:在 AppDelegate 的 didFinishLaunchingWithOptions 方法中請求授權(quán),iOS 8 之前决乎,本地推送 (UILocalNotification) 和遠程推送 (Remote Notification) 是區(qū)分對待的队询,應用只需要在進行遠程推送時獲取用戶同意。iOS 8 對這一行為進行了規(guī)范构诚,因為無論是本地推送還是遠程推送蚌斩,其實在用戶看來表現(xiàn)是一致的,都是打斷用戶的行為范嘱。因此從 iOS 8 開始送膳,這兩種通知都需要申請權(quán)限。iOS 10 里進一步消除了本地通知和推送通知的區(qū)別丑蛤。向用戶申請通知權(quán)限也變得十分簡單叠聋;
第一步: 導入 UserNotifications 框架
import UserNotifications
第二步: 在你要申請推送授權(quán)的地方,進行注冊推送通知 和 注冊let center = UNUserNotificationCenter.current() // 申請一個通知中心 center.delegate = notificationHandler // 將代理設置給我們自定義的一個自定義通知類受裹,該類專門用于管理推送設置相關(guān)屬性碌补、方法、代理方法 // 如果用戶需要跟推送過來的內(nèi)容進行交互棉饶,那么需要注冊 Action厦章,詳見下面方法 registerNotificationCategory() center.requestAuthorization(options: [.sound,.alert,.badge]){ // 請求授權(quán) granted,error in if granted { // 是否被授權(quán)成功 UIApplication.shared.registerForRemoteNotifications() // 注冊遠程推送,向 APNs 請求 token照藻; } else { if error != nil { print("授權(quán)的時候出現(xiàn)錯誤") } } }
第三步: (非必須)添加 Category 的 actions 方法
func registerNotificationCategory() {
if #available(iOS 10.0, *) {
let customUICategory: UNNotificationCategory = {
var collectedActionOption: UNNotificationActionOptions = .Foreground
ASUserManager.sharedInstance.hadLogin ? (collectedActionOption = .Destructive) : (collectedActionOption = .Foreground)
let viewDetailsAction = UNNotificationAction(
identifier: CustomizedUICategoryAction.viewDetails.rawValue,
title: NSLocalizedString("PushNotification_Action_ViewDetails", comment: "查看詳情"),
options: [.Foreground])
let collectedAction = UNNotificationAction(
identifier:CustomizedUICategoryAction.collected.rawValue,
title: NSLocalizedString("PushNotification_Action_Collected", comment: "收藏"),
options: [collectedActionOption])
return UNNotificationCategory(identifier: UserNotificationCategoryType.customizedCategoryIdentify.rawValue, actions: [viewDetailsAction, collectedAction], intentIdentifiers: [], options: [.CustomDismissAction])
}()
UNUserNotificationCenter.currentNotificationCenter().setNotificationCategories([customUICategory])
}
}
到這一步袜啃,我們打開我們的app,會出現(xiàn)以下授權(quán)提示框:
需要注意幸缕,我們可以點擊 不允許 或者 允許群发,而如果不是用戶很鐘愛的 app 的話,一般用戶都會點擊 不允許 发乔,而當你點擊 不允許 之后也物,你基本就享受不到這個 app 所有的推送通知服務(iOS 10 中包括本地推送),除非你到手機設置中重新打開該程序的推送通知按鈕
-
另外列疗,用戶可以在系統(tǒng)設置中修改你的應用的通知權(quán)限滑蚯,除了打開和關(guān)閉全部通知權(quán)限外,用戶也可以限制你的應用只能進行某種形式的通知顯示,比如只允許橫幅而不允許彈窗及通知中心顯示等告材。一般來說你不應該對用戶的選擇進行干涉坤次,但是如果你的應用確實需要某種特定場景的推送的話,你可以對當前用戶進行的設置進行檢查:
center.getNotificationSettings { (UNNotificationSettings) in // 還是在 didFinishLaunchingWithOptions 方法中添加該設置}
通過授權(quán)申請之后斥赋,我們可以通過 AppDelegate 的代理方法中拿到 deviceToken
// 該方法返回 deviceToken
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let tokenString = deviceToken.hexString
print("tokenString(tokenString)")
print("deviceToken(deviceToken)")
}
大家需要注意的是缰猴,拿到的 deviceToken 是 Data 類型的,其間會有空格隔開的疤剑,所以這里我使用了 Data 的拓展方法去掉了空格滑绒,如下:
extension Data {
var hexString: String { // 去除字符串間空格
return withUnsafeBytes {(bytes: UnsafePointer<UInt8>) -> String in
let buffer = UnsafeBufferPointer(start: bytes, count: count)
return buffer.map {String(format: "%02hhx", $0)}.reduce("", { $0 + $1 })
}
}
}
推送中期:開始真正意義上的推送 - NotificationViewController 的使用
- 淺談: NotificationViewController 其實我們可以在這里類的 main.storyboard 中自定義 UI 界面, 并拿到從 NotificationService (下個介紹的類)下載到磁盤的資源為 UI 賦值數(shù)據(jù)隘膘,讓界面活起來
-
使用步驟:
首先疑故,按照圖片路徑路徑新建一個 Target:
點擊創(chuàng)建如下文件,并命名為 NotificationViewController 類:
到這一步弯菊,你會發(fā)現(xiàn)項目中多了三個文件:
點開 info.plist 文件:
不得不解釋如下參數(shù):
UNNotificationExtensionCategory: 這里務必要和代碼中的 categoryID 一樣 纵势,否則推送無法識別其中點擊方法;
UNNotificationExtensionInitialContentSizeRatio:自定義 UI 界面在屏幕顯示時管钳,占屏幕的比例大星仗;
接下來我們先看看我創(chuàng)建好的文件 以及 我剛剛默默敲好的代碼
import UIKit
import UserNotifications
import UserNotificationsUI
class NotificationViewController: UIViewController, UNNotificationContentExtension {
@IBOutlet weak var descriptionImageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any required interface initialization here.
}
/**
1. 拿到后臺的推送通知內(nèi)容才漆,自定義顯示樣圖的 UI 樣式
2. - 若為遠程推送牛曹,我們必須通過 NotificationService 將后臺的資源下載到本地磁盤中
- 若為本地推送,一般情況下醇滥,我們也會把資源事先保存在本地磁盤中
故而黎比,由于資源在本地磁盤中,我們需要先獲得授權(quán)才可以訪問磁盤的內(nèi)容腺办,這里調(diào)用 startAccessingSecurityScopedResource 去獲得訪問權(quán)限
*/
func didReceiveNotification(notification: UNNotification) {
let content = notification.request.content
if let attachments = content.attachments.last {
if attachments.URL.startAccessingSecurityScopedResource() {
descriptionImageView.image = UIImage(contentsOfFile: attachments.URL.path!)
}
}
}
// 通過反饋焰手,用戶可以自定義觸發(fā)的 action 方法
func didReceiveNotificationResponse(response: UNNotificationResponse, completionHandler completion: (UNNotificationContentExtensionResponseOption) -> Void) {
if response.actionIdentifier == "action.viewDetails" { // 查看詳情
completion(.DismissAndForwardAction)
} else if response.actionIdentifier == "action.collected" { // 收藏
completion(.DismissAndForwardAction)
}
}
}
推送后期:推送即將結(jié)束 - NotificationViewService 的使用
淺談: 該類主要是便于用戶推送一些較為私密的信息糟描,這是 iOS 10 的一大亮點怀喉,解決了過去信息泄露的問題。其邏輯是船响,你可以在后臺推送一些私密信息過來該類躬拢,該類通過拿到信息之后,進行解密见间,甚至還可以修改這些信息(30 s 修改時間)聊闯,然后再保存到本地磁盤中,等待被 NotificationContent 類調(diào)用米诉。注意: iOS 10本地推送是不需要經(jīng)過該類的菱蔬,所以只在遠程推送的情況下,暢談該類才會有意義。
詳看該類的代碼結(jié)構(gòu): 默默的敲了一些代碼如下
在該類拴泌,我們需要解析后臺給我們的 JSON 數(shù)據(jù)魏身,并拿到所有的資源存儲到磁盤中,這里我通過 image 字段去拿到圖片資源蚪腐,并保存到磁盤箭昵,你也可以自定義你個人喜歡的字段。
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
var attachments: [UNNotificationAttachment] = []
override func didReceiveNotificationRequest(request: UNNotificationRequest, withContentHandler contentHandler: (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
if let bestAttemptContent = bestAttemptContent {
// Modify the notification content here...
if let userInfo = bestAttemptContent.userInfo as? [String: AnyObject], let imageString = userInfo["image"] as? String, let imageURL = NSURL(string: imageString) {
downloadImageToLocalWithURL(imageURL, fileName: "7.jpg", completion: { (localURL) in
if let localURL = localURL {
do {
// 在本地拿到縮略圖
if let thumbImageURL = NSBundle.mainBundle().URLForResource("thumbnailImage", withExtension: "png") {
do {
let lauchImageAttachment = try UNNotificationAttachment(identifier: "thumbnailImage", URL: thumbImageURL, options: nil)
self.attachments.insert(lauchImageAttachment, atIndex: 0)
} catch {
print("在拿到縮略圖的時候拋出異常\(error)")
}
}
let attachment = try UNNotificationAttachment(identifier: "thePushImage-\(localURL)", URL: localURL, options: nil)
self.attachments.append(attachment)
bestAttemptContent.attachments = self.attachments
contentHandler(bestAttemptContent)
} catch {
print("拋出異常: \(error)")
}
}
})
}
}
}
override func serviceExtensionTimeWillExpire() {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
let documentsDirectoryPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0]
private func downloadImageToLocalWithURL(url: NSURL, fileName: String, completion: (localURL: NSURL?) -> Void) {
guard let imageFormURL = self.getImageFromURLWithURLString(url.absoluteString!) else {
print("loacl image nil")
return
}
// 將圖片保存到本地中
self.saveImageToLoaclWithImage(imageFormURL, fileName: fileName, imageType: "jpg", directoryPath: self.documentsDirectoryPath) { (wasWritenToFileSucessfully) in
guard wasWritenToFileSucessfully == true else {
print("文件寫入過程出錯")
return
}
if let urlString = self.loadImagePathWithFileName(fileName, directoryPath: self.documentsDirectoryPath) {
completion(localURL: NSURL(fileURLWithPath: urlString))
}
}
}
private func getImageFromURLWithURLString(urlString: String) -> UIImage? {
if let url = NSURL(string: urlString) {
if let data = NSData(contentsOfURL: url) {
return UIImage(data: data)
}
}
return nil
}
private func loadImagePathWithFileName(fileName: String, directoryPath: String) -> String? {
let path = "\(directoryPath)/\(fileName)"
return path
}
private func saveImageToLoaclWithImage(image: UIImage, fileName: String, imageType: String, directoryPath: String, completion:(wasWritenToFileSucessfully: Bool?) -> Void) {
if imageType.lowercaseString == "png" {
let path = directoryPath.stringByAppendingPathComponent("\(fileName)")
if let _ = try? UIImagePNGRepresentation(image)?.writeToFile(path, options: NSDataWritingOptions.DataWritingAtomic) {
completion(wasWritenToFileSucessfully: true)
} else {
completion(wasWritenToFileSucessfully: false)
}
} else if imageType.lowercaseString == "jpg" || imageType.lowercaseString == "jpeg" {
let path = directoryPath.stringByAppendingPathComponent("\(fileName)")
if let _ = try? UIImageJPEGRepresentation(image, 1.0)?.writeToFile(path, options: NSDataWritingOptions.DataWritingAtomic) {
completion(wasWritenToFileSucessfully: true)
} else {
completion(wasWritenToFileSucessfully: false)
}
} else {
print("Image Save Failed\nExtension: (\(imageType)) is not recognized, use (PNG/JPG)")
}
}
}
private extension String {
func stringByAppendingPathComponent(path: String) -> String {
return (self as NSString).stringByAppendingPathComponent(path)
}
注意: 這里的 fileName 要盡量簡單些且以圖片的后綴進行命名(JPG回季、PNG...)家制,太復雜的 fileName ,系統(tǒng)會難以識別泡一。
本地推送測試
-
為 UNMutableNotificationContent 類設置相關(guān)信息
** UNNotificationAttachment:** 可以將本地的資源放在該類
的對象中并返回給 UNMutableNotificationContent 的 attachments(數(shù)組)颤殴;
categoryIdentifier:必須設置得和之前在 UNNotificationContent 的 info.plist 的 category 一樣,否則無效瘾杭;
UNTimeIntervalNotificationTrigger: 推送的時間和重復情況
最后诅病,只需要在本地的通知中心中添加通知請求即可生效;
// iOS 10 本地推送
if #available(iOS 10.0, *) {
let content = UNMutableNotificationContent()
content.title = "推送就得用這個標題才有用"
content.body = "推送的內(nèi)容: 在這里粥烁,你想說什么都可以"
let imageNames = ["AppSo_avatar","AppSo_Icon"]
let attachments = imageNames.flatMap { (name) -> UNNotificationAttachment? in
if let imageURL = NSBundle.mainBundle().URLForResource(name, withExtension: "jpeg") {
return try? UNNotificationAttachment(identifier: "image-(name)", URL: imageURL, options: nil)
}
return nil
}
content.attachments = attachments
content.categoryIdentifier = UserNotificationCategoryType.customizedCategoryIdentify.rawValue
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3,repeats: false)
let requestIdentifier = UserNotificationType.customizedInterfaceNotification.rawValue
let pushNotificationRequest = UNNotificationRequest(identifier:requestIdentifier, content: content, trigger: trigger)
UNUserNotificationCenter.currentNotificationCenter().addNotificationRequest(pushNotificationRequest){ error in
if error != nil {
print("come out a error,when add the push request")
} else {
print("customized UI Notificaiton scheduled: (requestIdentifier)")
}
}
}
遠程推送測試
網(wǎng)上很多第三方可以用于遠程推送測試贤笆,我們公司是使用 LeadCloud (測試流程見 leadCloud 官網(wǎng)),這里提供測試的 playload:
{
"aps": {
"alert":{
"title": "每日精選限免 APP",
"body": "夢境旋律:¥ 25 —> 0讨阻,首次限免芥永,appsoStore 本周限免,AppStore 本周限免钝吮,日式畫風的音樂游戲從戰(zhàn)場到宇宙埋涧,背負絕癥女孩踏遍夢境"
},
"mutable-content":1
},
"sound": "default",
"image": "https://upload-images.jianshu.io/upload_images/2691764-7859401c51e1e9b9.png",
"category": "AppSoPushCategory"
}
注意點1 : mutable-content 要為 1 ,系統(tǒng)才會讓通過 NotificationService 去下載圖片資源奇瘦,否則不會使用該類棘催;
注意點2 : image 資源路徑一定要是 https 的,否則無法生效耳标,還有圖片大小不宜過大醇坝,因為通過 NotificationService 下載圖片資源的時間很短,圖片資源太大次坡,系統(tǒng)會來不及下載呼猪;
注意點3 : category 要與代碼中的 category 一致 ,否則會導致找不到推送通知的路徑砸琅;
推送收尾:效果圖樣
推送過程出現(xiàn)的 bugs
當在 Xcode 8 swift 2.3 或者以下版本的情況下創(chuàng)建新類:NotificationService 和 NotificationViewController 時都會出現(xiàn)版本不兼容的情況宋距,因為我們創(chuàng)建代碼出來是 swift 3.0 的,但我們的代碼環(huán)境并不是 3.0 的症脂,故而會出現(xiàn)這種情況谚赎,解決方法是將如下鍵設置為 YES: