iOS App 有著各式各樣的功能,但這當中如果要投票選一個讓人又愛又恨的功能,絕對非推播莫屬。

如上圖所示,每天我們會收到來自不同 App 的推播訊息。我們恨推播,因為很多都是煩人的廣告訊息;但有時我們卻又愛推播,比方彼得潘想跟溫蒂告白,傳 LINE 訊息問她要不要在一起時,期待馬上就能收到 LINE 的推播,看到溫蒂 Say YES。
不管彼得潘能不能告白成功,為了讓天下有情人都能終成眷屬,接下來彼得潘將教大家實作一個接收播推的 App,相信到時候你用自己親手做的 App 推播告白訊息,對方一定會被你的真心感動的。 (如果對方不回應,你可以每天照三餐推播,總有一天她會理你的。)
Local notification 和 Remote notification
iOS 上的推播,分為 local notification 和 remote notification 兩種。Local notification 和時間或位置有關,需要事先設定,直接由本機端觸發。比方每天下午六點提醒我們該下班的推播,或是靠近 101 Apple Store 出現的 iPhone X 廣告推播。Remote notification 則透過網路傳送到我們的手機,因此它比較彈性,任何時候你心血來潮,都可以發送新的推播訊息。
接下來我們的告白推播將以 remote notification 實作。當我們半夜睡不著覺時,不如就爬到屋頂傳推播給對方,讓對方也睡不著吧。
Remote notification 的傳送對象
Remote notification 傳送的對象,可以分為以下三種:
- 某個人 – 傳給特定對象,比方使用 FB, LINE 等聊天 App 傳早安訊息給暗戀的女生。
- 所有人 – 傳給所有安裝了 App 的人,比方誠品 App 在週年慶時傳送推播,提醒大家 Swift 書籍 5 折優惠。
- 滿足某條件的人 – 傳給部分的人,比方 Sogo 百貨 App 在三月時傳推播給三月生日的壽星,提醒她來店換取生日禮物。
Remote notification 的限制
Remote notification 很強大,不過它並非無所不能,為了避免你期待太高,之後反而失望,最好先了解它的一些限制。
- 付費成為開發會員才能發送推播 – 推播要透過 Apple 的 server 發送,Apple 當然不會無緣無故幫我們呀,所以請先付錢給他,他才會幫我們發送。
- 不能在模擬器測試 – 推播只能傳送到實機上,無法發送到模擬器,所以請在 iPhone, iPad 或 iPod Touch 測試。
- 有可能沒收到推播訊息 – 雖然大部分的時候,我們都能收到推播訊息,但請記得它有可能失敗。比方如果你發送推播時,對方剛好在爬喜馬拉雅山,iPhone 沒網路,那她當然收不到呀。雖然 Apple 的 server 會幫我們保留傳送失敗的推播,之後再重送,但它只會保留一段時間,過期的推播將被可憐地清掉。因此若她不幸在深山裡迷路,一個星期收不到網路,就算她之後找到回家的路,下山有了網路,也收不到之前漏接的推播。
類似郵差寄信的推播傳送過程
在開始實作 remote notification 之前,先讓我們認識一下推播訊息如何經歷千辛萬苦,傳遞到 iOS 裝置上的 App。

推播傳送的過程,其實跟真實世界的寄信很像。如上圖所示,一開始有個 provider server 負責發送推播,它的角色就像寄件者。一般來說,它會是我們自己的後台,因此我們可從後台的程式控制,自由決定推播的內容以及何時發送,比方過年時發送推播給溫蒂,跟她說恭喜發財,紅包拿來。
但在推播傳送的過程中,最重要的角色卻是 Apple 的 server,APNs (Apple Push Notification Service) Server,因為它就像郵差,只有它知道如何將推播送達 iOS 裝置上的 App。
因此,provider server 會先將推播傳送到 APNs server,然後 APNs server 再將推播傳送到 iOS 裝置,最後 iOS 再將推播傳給接收的 App。對比寄信的情境,iOS 裝置就像屋子的信箱,App 就像收件者。
現在我們了解了推播的流程,但 APNs server 怎麼知道推播要送到地球上哪一個 iOS 裝置 的 App 呢 ? 寄信時我們透過地址指定接收的對象,推播也是一樣的概念,只是它不是房子的地址,而是一個稱為 device token 的字串。之後在實作時,我們將對它有更進一步的認識。
接下來,就讓我們趕緊開始實作推播告白 App,才不會被虎克船長搶先用 Android App 跟溫蒂告白呀。
讓 App 具有接收推播的能力
首先,我們在 App 的 General 頁面將 Team 設為付費帳號。記住,只有付費的開發會員能發送推播 !

然後,在 App 的 Capabilities 頁面打開 Push Notifications 。

App-capabilities
打開開關後,它會幫我們做以下兩件事:
- 以 Xcode 裡設定的 Bundle ID,在 Apple 的開發網站建立 App ID。


2. 產生推播需要的 entitlements 檔。

取得推播權限
不是每個人都喜歡推播,所以我們一定要經過使用者同意後,才能發送推播。
接下來就讓我們在 App 一啟動時,勇敢地詢問她是否願意接收通知吧。
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in
}
return true
}
利用 UNUserNotificationCenter
的 current() 取得 UNUserNotificationCenter
物件後,呼叫它的 function requestAuthorization(options:completionHandler:)
,請求使用者允許 App 傳送通知。另外別忘了要先 import UserNotifications
,如此才能順利使用推播的相關型別。
import UserNotifications
接著讓我們再仔細看一下 requestAuthorization(options:completionHandler:)
,了解它的參數 options。
func requestAuthorization(options: UNAuthorizationOptions = [], completionHandler: @escaping (Bool, Error?) -> Void)
在請求使用者同意通知時,我們必須透過參數 options 告知通知會包含哪些東西。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 }
}
為了增加告白成功的機率,我們希望通知同時包含浪漫的文字,好聽的聲音,並以 App Icon 上的數字表示這是第幾次告白,因此我們傳入 [.alert, .sound, .badge]。

使用者看到的通知請求將如上圖所示,當她經過仔細思量,終於選了允許或不允許後,將觸發 function requestAuthorization
的參數 completionHandler 執行,granted 說明是否允許,允許時值為 true,error 則說明錯誤的原因。
一旦使用者做了選擇,不管允許還是不允許,之後通知請求的訊息都不會再出現,除非她將 App 移除並重新安裝。若她改變心意,比方原本允許,後來發現你是邊緣人,想要封鎖你,也是可以,只是她不能從原來的 App 改變,而要從設定 App 的通知頁面變更。

向 Apple 註冊,請求接收推播
App 想要接收推播,原來一點也不簡單,不只要使用者同意,也要 Apple 同意,就好像彼得潘想追到溫蒂,不只要她同意,也要她的父母同意,不然只能私奔到月球了。
如以下程式碼,我們在 App 啟動時呼叫 UIApplication 物件的 function registerForRemoteNotifications
, APNs server 註冊,請求接收推播。
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in
}
UIApplication.shared.registerForRemoteNotifications()
return true
}
一旦註冊成功,protocol UIApplicationDelegate 的 function application(_:didRegisterForRemoteNotificationsWithDeviceToken:)
將被呼叫。我們可在 AppDelegate 裡定義此 function,讀取它宇宙無敵重要的參數 deviceToken。
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
}
前面提過推播傳送時,device token 就像地址,有了它才知道通知要送給誰。但是別忘了,通知得先從我們的後台 provider server 發送,provider 就像寄件者,他在發送推播時,就要指定 token,到時候郵差 APNs server 才知道要寄給誰。因此一般來說,我們會在此 function 裡將 token 傳到我們的後台。
但是請注意,剛剛 function 的參數 deviceToken 型別為 Data,我們習慣先將它轉換為字串後再上傳。此字串的格式必須為十六進位,如以下 Apple 文件裡的範例,00fc13adff785122b4ad28809a3420982341241421348097878e577c991de8f0 代表 token,它將被帶在推播封包裡 HTTP/2 request 的 :path 欄位。

以下為將 deviceToken 轉換為十六進位字串的程式碼。由於 deviceToken 型別為 Data,因此我們用 for in 將每個 byte 的內容當成 UInt8 型別讀取,然後再利用 String(format: “%02x”, byte),將數字變成十六進位的字串後,一個個組合起來。%02x 的 x 代表十六進位,02 表示將以兩個數字表示,若內容不足兩位數,第一位將補 0,比方 a 將變成 0a。
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
var tokenString = ""
for byte in deviceToken {
let hexString = String(format: "%02x", byte)
tokenString += hexString
}
}
如果熟悉 function reduce 的朋友,也可以用以下寫法組合出十六進位的字串。
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let deviceTokenString = deviceToken.reduce("") {
return $0 + String(format: "%02x", $1)
}
}
成功將 token 轉換為字串後,之後即可撰寫上傳的程式碼,將它上傳到我們的後台。對很多 App 來說,token 會對應到某個使用者。比方彼得潘想跟溫蒂和虎克船長告白(虎克船長是備胎),先請他們兩個安裝 App。到時候他們的 App 得到 token,上傳到後台時,後台將記錄 token A 是溫蒂,token B 是虎克船長,如此當彼得潘從後台控制發送推播給溫蒂時,才知道要搭配 token A。
關於 device token,還有以下幾點特別的地方要注意:
- Token 對應到 iOS 裝置的 App – 同一個 iOS 裝置上,不同的 App 會得到不同的 token。
- 當使用者不同意接收通知時,App 無法取得 token – 換句話說,function
application(_:didRegisterForRemoteNotificationsWithDeviceToken:)
不會被呼叫。 - Token 可能會變,記得更新 – 就像人會變心一樣,token 也會變,所以我們最好在每次 App 啟動時呼叫 registerForRemoteNotifications,如此一旦 token 改變,觸發
application(_:didRegisterForRemoteNotificationsWithDeviceToken:)
時,我們才能立即上傳後台,確保 App 能收到通知。 - 跟 APNs server 註冊接收推播失敗時,將觸發以下 function。
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
}
現在 App 已經具備接收推播的能力,接下來,就讓我們連到 Apple 開發網站,進行發送推播需要的其它設定。
產生 APNs Key
想要從 provider server 發送推播,我們還需要 APNs key。下載 APNs key 的步驟如下:
- 登入 Apple 開發網站,點選 Certificates, Identifiers & Profiles.

2. 點選 Keys 下的 All 後,點選右上角的 + 新增 key。

3. 輸入 key 的名字,勾選 APNs 後,點擊 Continue 按鈕。

APNs 裡的說明文字提到 One key is used for all of yours apps,因此就算我們有一百個 App 要發送推播,都可以共用同一個 key。除此之外,APNs key 也不會過期,一旦生成,你可以用一萬年!
4. 點選 Confirm 按鈕完成 key 的新增。

5. 點選 Download 下載 key。


下載的 key 將是附檔名 p8 的檔案。請特別注意這段英文文字:After downloading your key, it cannot be re-downloaded as the server copy is removed。此 key 十分珍貴,只能被下載一次,因此下載後請好好保存,如果弄丟只能重新產生新 key。

如果將來你不喜歡 key 了,想將它刪除,只要點選想移除的 key 後,按 Revoke 按鈕即可。刪除前請三思,因為一旦刪除,它將不能再拿來發送推播。


利用 Node.js 程式當 provider server 發送推播
有很多方法可以實現 provider server,你可以用 PHP, Ruby on Rails 等開發 server 程式,或是利用第三方的後台,比方 Firebase。接下來我們將直接在 Mac 上執行 Node.js 程式,實際示範如何發送推播。本部份的重點在測試推播的發送,所以你不需要特別學習 Node.js 的語法,只要依步驟操作修改即可。
為了執行 Node.js 程式,我們的 Mac 必須先安裝 Node.js。如果你在 terminal 輸入指令 node 按 Enter 後,出現 command not found,即表示尚未安裝。
要安裝 Node.js,我們要先連到 Node.js 的網站,點選 macOS Installer 下載 Node.js 的 pkg。

點擊 Node.js 的 pkg 進行安裝。



安裝成功後,就可以在 terminal 順利輸入 node 指令。

下一步,我們要建立傳送推播的 Node.js 程式資料夾 apns。
mkdir apns
cd apns
然後安裝發送推播的套件,apn。關於套件 apn 的詳細介紹,可參考連結。
npm init --yes
npm install apn --save
再將剛剛下載的 key (p8 檔)放到 apns 資料夾下。

在 apns 資料夾下撰寫發送推播的 js 檔,設定 key, keyId, teamId。
比方檔案取名為 push.js,輸入以下內容:
var apn = require('apn');
var options = {
token: {
key: "AuthKey_9943HQBBPS.p8",
keyId: "9943HQBBPS",
teamId: "G4HL98LX6L"
},
production: false
};
var apnProvider = new apn.Provider(options);
Key 填入 p8 檔的檔名,keyId 可從 Apple 的開發網站點擊 key 後查詢。

而 teamId 則可在 Apple 開發網站的 Membership 頁面找到。

看一看剛剛的 js 檔,有個 production 欄位,它代表什麼意思呢 ?
它和 APNs server 有關,APNs server 有兩種,development 和 distribution。Development server 的網址是 api.development.push.apple.com:443,我們平常從 Xcode 開發裝到手機的 App,收到的推播來自 development server。而 distribution server 的網址為 api.push.apple.com:443,我們從 App Store, TestFlight, AdHoc 安裝的 App,則從 distribution server 接收推播。
待會測試時,我們將從 Xcode 安裝 App 到 iPhone,因此我們將 production 設為 false。之後當我們 App 上架,想送推播給下載正式 App 的使用者時,再將 production 設為 true。
也許記憶力好的讀者會想到,剛剛出現過的 entitlements 有個 APS Environment 欄位,內容是 development,應該就代表它可以接收來自 development server 的推播吧。

沒錯,的確如此,那如果之後上架的 App 要接收來自 production server 的推播,是不是要再手動改成 production 呢 ? 感謝貼心的 Xcode,它幫我們做好了,當我們從 Xcode 的 Archive 製作上架的 App 時,APS Environment 欄位將被自動改成 production。

不僅於此,當我們用 Archive 製作 App Store, TestFlight, AdHoc 版本的 App 時,它還會自動幫我們產生 distribution profile 呢。

之後,我們要設定 device token 和推播的內容。
在剛剛的 push.js 檔的最後加入以下程式。
var note = new apn.Notification();
let deviceToken = "d75e441c494714ec89f57b58bf685bf4166032214715eef86c790536ac60c63e"
note.alert = "Wendy, I love you";
note.sound = "default";
note.badge = 1;
note.topic = "com.pegterpan.LoveConfession";
apnProvider.send(note, deviceToken).then( (result) => {
// see documentation for an explanation of result
});
前面我們看到 App 如何取得 device token 的十六進位字串。為了測試方便,我們在此直接將 deviceToken 設為 App 取得的 token 字串。
Topic 欄位要輸入 App 的 bundle ID。至於推播的內容,最常設定的是以下三個欄位:
- Alert: 推播的文字內容。
- Sound: 推播的聲音檔名,default 表示預設的推播聲。沒有指定 sound 的話,將變成無聲推播。如果你覺得自己唱歌很好聽,也可以錄成音檔,以自訂的音檔當推播聲 (不能超過 30 秒)。
- Badge: App Icon 上顯示的數字。
最後,我們可以在 terminal 輸入 node push.js 發送推播。

一切順利的話,此時溫蒂應可在 iPhone 上看到感人的 I love you。
推播內容的真面目 – JSON Dictionary
推播的內容其實是我們熟悉的 JSON 格式,剛剛例子裡設定的 alert,sound 和 badge,最後將變成 dictionary key aps 下的欄位。
{
"aps" : {
"alert" : "Wendy, I love you",
"badge" : 1,
"sound" : "default"
}
}
在 aps 下可以設定的欄位不僅於此,比方 content-available 可以實現使用者看不見的背景更新推播,silent notification,讓 App 在背景收到通知後,執行某項工作,比方連到後台抓取新資料。對於推播欄位的好奇寶寶,可進一步參考 Apple 官方文件的說明。
除了 Apple 在 aps 下定義的欄位,我們也可加入自訂的欄位,放在跟 aps 同一層,方便到時候 App 收到推播時,解讀內容做對應的動作。
{
"aps" : {
"alert" : "Wendy, I love you",
"badge" : 1,
"sound" : "default"
}
"link": "http://apppeterpan.strikingly.com"
}
比方以上例子,link 欄位放上彼得潘網站的網址,到時候使用者點選推播,打開 App 時,即可讓 App 的程式顯示 link 欄位對應的帥氣網頁。
呈現聲音,圖片和影片的 Rich Notification
iOS 的推播不斷進化,目前已可支援 Rich Notification,呈現酷炫的聲音、圖片和影片,不過並非每個格式都支援,下表為它支援的格式和大小限制。

但是聲音、圖片和影片要怎麼包含在推播的 JSON 裡呢 ? 基本上我們不會將它們直接放在 JSON 裡,因為推播的內容有大小限制,一般的上限是 4KB。
我們會在 JSON 裡將 mutable-content 設為 1,然後搭配 Notification Service Extension,讓 App 收到推播時,先在背景修改內容後再顯示,比方解密被加密的推播訊息,或從圖片或影片的網址,下載資料後顯示在推播上。對此部分有興趣的朋友,可參考 WWDC16 的 Advanced Notifications 和 Apple 官方文件。
{
"aps" : {
"alert" : "Wendy, I love you",
"badge" : 1,
"sound" : "default"
"mutable-content" : 1
}
"image": "https://images-na.ssl-images-amazon.com/images/I/51f-7KjjFeL._SX317_BO1,204,203,200_.jpg"
}
點選推播和收到推播時觸發的 function
最後,我們來聊聊收到推播時,如何讓 App 的程式執行 ? 由於到時候觸發的 function 來自 protocol UNUserNotificationCenterDelegate,所以我們先在 application(_:didFinishLaunchingWithOptions:)
裡加入以下程式,將 AppDelegate 物件設為 UNUserNotificationCenter 的代理人。
UNUserNotificationCenter.current().delegate = self
接著定義以下 2 個 UNUserNotificationCenterDelegate
的 function。
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
print("didReceive", response.notification.request.content)
completionHandler()
}
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
print("willPresent", notification.request.content)
completionHandler([])
}
}
當使用者點選推播打開 App 時 (App 在背景或是沒被啟動),將觸發 userNotificationCenter(:didReceive:withCompletionHandler:)
。而當 App 本來就在前景,收到推播時則會觸發 userNotificationCenter(:willPresent:withCompletionHandler:)
。不管是哪一種,我們都可透過參數取得 UNNotificationContent
物件,解析推播的內容。
以上兩個 function,當我們完成想做的事情後,都要呼叫 completionHandler。但在前景收到推播觸發的 userNotificationCenter(_:willPresent:withCompletionHandler:)
比較特別一點,它的 completionHandler 還可以傳入參數,控制是否在前景顯示推播,比方 completionHandler([])
代表不顯示, completionHandler([.alert])
則可顯示推播文字。
總結
以上是關於 Apple push notification 的簡單介紹。若能模仿以上步驟操作,應該就能順利開發出接收推播的 App。當然,push notification 還有很多進階的功能,比方客製推播 UI 畫面的 Notification Content Extension、iOS 11 的隱藏推播訊息 (hidden notification content) 等,有興趣的朋友,都可再進一步研究相關文件。
關於 Swift iOS App 開發的相關技術,大家若有任何問題,都可在底下留言,或是直接 FB / LINE 聯絡彼得潘。當彼得潘回答大家的問題時,其實也在找答案的過程中精進學習,增長了自己的功力,和大家交了朋友,獲得再多錢也買不到的回報和收獲。