
在i OS 10 之前,使用者通知都是很單調且簡單的,沒有豐富的圖片或多媒體,單純是以文字格式顯示。依照使用者的設定,通知可以顯示在螢幕鎖定畫面或主畫面中,若是使用者遺漏了任何一則通知,可以開啟通知中心來查看所有待處理的通知,如圖 29.1 所示。

自 iOS 10 版本發布以來,Apple 改版通知系統,支援豐富內容以及自訂通知 UI 的使用者通知。「豐富內容」即表示你可以在通知中加入靜態圖片、動畫 GIF、影片與音樂,圖 29.2 向你展示了豐富內容通知的概念。

你也許聽過推播通知( Push Notification ),其已在通訊 App 中廣為採用。實際上,使用者通知可以分成兩種類型:「本地推播通知」( Local Notifications)與「遠端推播通知」( Remote Notifications )。「本地推播通知」是由應用程式本身來觸發,並收納在使用者的裝置中,例如:基於位置的 App 會在使用者位於特定地區時向使用者發送通知,或者待辦事項 App 會在某個項目接近截止日期時顯示通知。
遠端推播通知通常是由遠端伺服器的伺服端應用程式所啟動,當伺服器應用程式想要傳送訊息給使用者,它會向Apple 推播通知服務(簡稱 APNS)發送通知,然後這個服務會轉發通知至使用者的裝置上。
本章將不討論遠端推播通知的實作,而是將重點放在本地推播通知,並且向你展示如何使用新的使用者通知框架來實作豐富內容的通知。
那麼我們要為 FoodPin App 加上什麼功能呢?使用本地推播通知是提醒使用者注意你的 App 的絕佳方式。最近一項研究指出,只有不到 25% 的人會多次使用某個 App,換句話說,超過 75% 的使用者在下載 App 後開啟它,之後就不曾再使用過了。
More than 75% of App Downloads Open an App Once And Never Come Back
by Erin Griffith
在 App Store 中有超過 200 萬個 App,要讓人注意到並下載你的 App 已是很難了,更何況是讓人持續使用又更加困難。善用使用者通知,可以幫助你留住使用者,並改善 App 的使用者體驗。
使用者通知框架提供了不同的觸發器來啟動本地推播通知:
FoodPin App 是專為美食愛好者設計,可以將他們最愛的餐廳加到收藏夾,如果 App 能夠在使用者到達某個地點時推薦使用者最愛的餐廳,是不是很棒呢?舉例而言,你已經儲存了東京數間餐廳,當你到達東京時,這個 App 會觸發通知,顯示這個城市中你最喜歡的餐廳清單。
或者你可以使用基於行事曆的觸發器,在假期來臨前啟動通知(例如:聖誕節前 10 天)。通知可以這樣寫:
「嗨!聖誕節前夕,是時候利用假期和朋友一起聚餐了。這裡有一些你最喜歡的餐廳, 你可以參考看看。」
上述是一些示範的範例,使用者在看到通知後,會更有意願返回使用App。
為了讓這本初階書內容保持簡單,我們將不實作上述的觸發器;相反的,我將向你展示如何使用基於時間的觸發器來觸發本地推播通知。也就是說,這些通知並非是無用或是像垃圾郵件,一旦你了解使用者通知框架的基礎知識,要實作其他類型的觸發器就不會太困難。
我們準備要做的是,當使用者上次使用App 後經過一段時間(例如:24 小時),我們會發送通知並推薦使用者一間餐廳。此外,我們將讓使用者和通知進行互動,當使用者看到通知時,他可以選擇是否要訂位,若是使用者點擊該按鈕,就會直接撥打餐廳電話,圖 29.3 顯示了一個通知範例。

看起來是不是很棒呢?我們將開始並說明如何在你的 App 中發出通知。
「使用者通知框架」是用來管理和排程通知的框架。要實作使用者導向的通知,首先在你的程式碼中匯入這個框架,以便你可以存取框架綁定的 API。
在 FoodPinApp.swift 中插入下列這行程式碼:
import UserNotifications
不管通知的類型為何,你都必須先請求使用者授權與許可,然後才能向使用者的裝置發送通知。我們通常會在 AppDelegate 類別中採用 application(_:didFinishLaunchingWithO ptions:) 方法來實作授權請求。在 AppDelegate 類別中插入下列的程式碼:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in
if granted {
print("User notifications are allowed.")
} else {
print("User notifications are not allowed.")
}
}
return true
}
使用授權請求提示使用者非常簡單,從上列的程式碼可以看到,我們對與這個 App 有關的 UNUserNotificationCenter 物件呼叫 requestAuthorization,而且我們要求顯示提示、播放聲音,並更新 App 圖示的徽章通知( Badge )。
現在執行這個專案來測試它。當 App 啟動時,你應該會看到一個授權請求,一旦你接受後,App 便可以向裝置發送通知。若要驗證通知的設定,你可以至「Setting → FoodPin → Notifications」中做確認,如圖 29.4 所示。

現在 FoodPin App 已經準備好發送通知給使用者。我們先了解一下 iOS 中通知的基本外觀,通知的最上面是標題,下一行是副標題,接著是訊息的本文,如圖 29.5 所示。

使用者通知的內容是由 UNMutableNotificationContent 表示。要建立內容,你需要實例化一個 UNMutableNotificationContent 物件,並將其屬性設定為適當的資料。舉例如下:
let content = UNMutableNotificationContent()
content.title = "Restaurant Recommendation"
content.subtitle = "Try new food today"
content.body = "I recommend you to check out Cafe Deadend."
如果你想要觸發通知時播放聲音,也可以設定內容的 sound 屬性:
content.sound = UNNotificationSound.default()
排程通知非常簡單,使用你偏好的觸發器建立 UNNotificationRequest 物件,然後將請求新增至 UNUserNotificationCenter。我們來看下列的程式碼片段,這是排程通知所需的程式碼。
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 10, repeats: false)
let request = UNNotificationRequest(identifier: "foodpin.restaurantSuggestion", content: content, trigger: trigger)
// 排程通知
UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
如前所述,我們要在一段時間後觸發通知。我們建立 UNTimeIntervalNotificationTrigger 物件,並將時間間隔設定為特定值(例如:10 秒),然後我們以通知內容及觸發器來建構一個 UNNotificationRequest 物件, 你必須為請求指定一個唯一識別碼。之後, 若你想要移除或更新通知,則可以使用識別碼來識別通知,最後你使用通知請求呼叫 UNUserNotificationCenter 的 add 方法來排程通知。
現在你應該對於如何建立與排程通知有些概念了,我們來實作餐廳的推薦通知。開啟 RestaurantListView.swift,然後在 RestaurantListView 結構中插入下列的方法:
private func prepareNotification() {
// 確保餐廳陣列不為空值
if restaurants.count <= 0 {
return
}
// 隨機選擇一間餐廳
let randomNum = Int.random(in: 0..<restaurants.count)
let suggestedRestaurant = restaurants[randomNum]
// 建立使用者通知
let content = UNMutableNotificationContent()
content.title = "Restaurant Recommendation"
content.subtitle = "Try new food today"
content.body = "I recommend you to check out \(suggestedRestaurant.name). The restaurant is one of your favorites. It is located at \(suggestedRestaurant.location). Would you like to give it a try?"
content.sound = UNNotificationSound.default
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 10, repeats: false)
let request = UNNotificationRequest(identifier: "foodpin.restaurantSuggestion", content: content, trigger: trigger)
// 排程通知
UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
}
為了更好管理程式碼,我們建立了 prepareNotification() 方法來處理使用者通知。我們之前已經瀏覽過大部分的程式碼,但是第一行程式碼對你而言是新的,這裡我們想從最愛的餐廳中隨機選擇一間餐廳,並推薦給使用者。下列這行程式碼是用來產生亂數:
let randomNum = Int.random(in: 0..<restaurants.count)
random(in:) 函數是用於產生亂數,它接受一個數字範圍,並在這個範圍內產生一個亂數。在上列的程式碼中,假設你最愛的餐廳有10 間,這個函數將隨機產生一個 0 至 9 之間的數字。有了亂數,我們就可以從陣列中選擇出一間推薦的餐廳,並建立通知內容。
要注意的是,我們設定時間間隔為 10 秒,這是為了示範、方便測試才如此設定,實際上這樣的時間太過於短暫,你可能希望在 24 小時(24×60×60 秒)或更長時間後觸發通知:
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 86400, repeats: false)
現在 prepareNotification 方法已經準備好了,你可以將 task 修飾器加到 NavigationStack 來呼叫它,如下所示:
.task {
prepareNotification()
}
很棒 !我們來執行這個專案,並快速進行測試。這個通知不會在 App 內顯示,因此啟動 App 後,請返回主畫面或是鎖定畫面,接著等待 10 秒鐘,你應該會看到通知。
當通知顯示在鎖定畫面上,你可以滑動通知來返回 FoodPin App。而當裝置解鎖時,這個通知會以橫幅的形式由上往下移動,你可以進一步向下滑動它來顯示完整內容,或者只需點擊通知即可跳回 App,如圖 29.6 所示。

我們從本章一開始就討論了豐富內容的通知,到目前為止,我們建立的通知都是純文字形式的,那麼該如何將推薦餐廳的圖片綁定到通知中呢?
這很簡單,設定 UNMutableNotificationContent 物件的 attachment 屬性即可:
content.attachments = [attachment]
attachments 屬性接受一個 UNNotificationAttachment 物件的陣列,以便與通知一起顯示。附件( attachment )可以包括圖片、聲音、音樂與影片檔。
請注意,你需要提供附件的 URL。在我們的例子中,它指的是推薦餐廳的圖片檔。
如果你還記得的話,Restaurant 的image 屬性是 Data 型別,那麼為了建立附件物件,我們該如何從圖片資料中建立圖片檔呢?
我們先檢視建立附件的程式碼,你可以在 prepareNotification 方法中(在觸發器實例化之前)插入下列程式碼:
// 新增圖片
let tempDirURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let tempFileURL = tempDirURL.appendingPathComponent("suggested-restaurant.jpg")
try? suggestedRestaurant.image.jpegData(compressionQuality: 1.0)?.write(to: tempFileURL)
if let restaurantImage = try? UNNotificationAttachment(identifier: "restaurantImage", url: tempFileURL, options: nil) {
content.attachments = [restaurantImage]
}
iOS SDK 提供一個名為「jpegData(compressionQuality:)」的內建函數,用來將圖片資料轉換成 JPEG 圖片檔。在上列的程式碼中,我們首先找到可存放圖片的暫時目錄, NSTemporaryDirectory() 函數回傳暫存檔的目錄路徑,然後我們將暫存檔名稱設定為「suggested-restaurant.jpg」。如果你將檔案路徑列印到主控台,它將顯示如下:
file:///Users/simon/Library/Developer/CoreSimulator/Devices/DC573158-103F-4D1B-8489-742E3C651D33/data/Containers/Data/Application/C2386E9A-48F7-411B-B485-95EC07CA0D8E/tmp/suggested-restaurant.jpg
jpegData(compressionQuality:) 函數回傳 JPEG 格式的圖片資料,然後將圖片資料寫入 JPEG 檔,我們利用此檔建立UNNotificationAttachment 物件,並將其指定給通知的 attachments 屬性。
現在,是時候來再次測試 App 了,執行專案並在模擬器中開啟 App。請記得導覽回到主畫面並等待通知出現,這次你應該會在通知中看到一個小縮圖,向下滑動來檢視大圖, 如圖 29.7 所示。

目前使用者只有一種方式可和通知互動,即點擊來啟動 App,如果你未提供自訂的實作,就會以此作為預設動作。
「互動式通知」(Actionable Notifications )能在不切換到 App 的情況下回應通知。互動式通知的最佳範例就是提醒事項 App,當你收到來自提醒事項App 的通知時,你可以選擇直接從通知管理提醒事項,你可以將其標示為已完成或者重新排程提醒事項,而無須啟動 App。
藉由使用者通知框架,我們可以對來自FoodPin App 的通知實作自訂動作。當通知出現在螢幕上,它會提供兩個選項供使用者選擇:
要實作自訂動作,你必須建立一個 UNNotificationAction 物件,並將其與通知類別關聯。動作物件需要一個唯一識別碼與一個標題(例如:訂位),這將顯示在動作按鈕上。
或者,你可以指定應如何執行動作。預設上,該動作是一個背景動作,它會取消通知並在背景執行你的自訂工作。例如:如果我們在程式碼中定義「稍後」( Later)動作,如下所示:
let laterAction = UNNotificationAction(identifier: "foodpin.cancel", title: "Later", options: [])
如果選擇「稍後」(Later )選項,則動作便會取消通知,因此我們在建立動作物件時, 不提供其他的選項。
另一方面,「訂位」(Reserve a table )動作是一個前景動作,因為我們必須將 App 帶到前景才能撥打電話,因此該動作會像這樣實作:
let makeReservationAction = UNNotificationAction(identifier: "foodpin.makeReservation", title: "Reserve a table", options: [.foreground])
設定動作物件後,將其與類別關聯:
let category = UNNotificationCategory(identifier: "foodpin.restaurantaction", actions: [makeReservationAction, cancelAction], intentIdentifiers: [], options: [])
你為類別提供唯一的識別碼,並傳送動作物件來關聯該類別,當你準備好類別後,將其註冊到UNUserNotificationCenter 物件,如下所示:
UNUserNotificationCenter.current().setNotificationCategories(["foodpin.restaurantaction"])
現在我們已經建立了動作,並將它們註冊到通知中心,不過這些動作尚未和通知關聯在一起。為此,你只需要將類別識別碼設定為通知內容的 categoryIdentifier 屬性即可:
content.categoryIdentifier = "foodpin.restaurantaction"
這就是你為使用者通知實作自訂動作所需的程式碼。插入下列的程式碼片段到 prepareNotification 方法中(放在trigger 變數之前):
// 新增動作
let categoryIdentifer = "foodpin.restaurantaction"
let makeReservationAction = UNNotificationAction(identifier: "foodpin.makeReservation", title: "Reserve a table", options: [.foreground])
let cancelAction = UNNotificationAction(identifier: "foodpin.cancel", title: "Later", options: [])
let category = UNNotificationCategory(identifier: categoryIdentifer, actions: [makeReservationAction, cancelAction], intentIdentifiers: [], options: [])
UNUserNotificationCenter.current().setNotificationCategories([category])
content.categoryIdentifier = categoryIdentifer
上列的程式碼和我們剛才討論的程式碼相同。現在,是時候執行 App 並測試通知動作,當通知橫幅出現時,向下滑動它,你便會看到已實作的自訂動作,如圖 29.8 所示。

如果你點擊「Reserve a able」按鈕,它會開啟 FoodPin App,不過請注意它不會自動為你打電話給餐廳。如前所述,動作物件有一個選項用來確定動作該如何執行。對於「Later」按鈕,我們未提供任何其他的選項,所以預設是取消通知;而對於「Reserve a table」按鈕,我們設定選項為「.foreground」,這會在點擊時把App 帶到前景來。
那麼當 App 返回前景時,我們該如何處理這個動作呢?
使用者通知框架中的 UNUserNotificationCenterDelegate 協定便是為了這個目的而設計的,這個協定定義了一個回應互動式通知的方法:
optional func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void)
要處理動作並執行自訂程式碼,你需要在一個委派物件中實作協定,並將其指定給通知中心物件,當App 返回前景時,將相應地呼叫該方法。
我們將在 AppDelegate 中實作這個協定,但在我們這樣做之前,你可能還有另一個問題,我們要打電話給推薦的餐廳,該如何從 RestaurantListView 傳送餐廳的電話號碼至 AppDelegate 呢?
通知內容有一個名為「userInfo」的屬性,可以讓你以字典的格式儲存自訂資訊。例如: 你可以在通知中儲存電話號碼,如下所示:
content.userInfo = ["phone": suggestedRestaurant.phone]
將上列這行程式碼放在 prepareNotification 方法中的 content.sound 下方。
透過與通知關聯的電話號碼,現在我們來實作 UNUserNotificationCenterDelegate 協定。開啟 FoodPinApp.swift,並編輯 AppDelegate 來採用這個協定,如下所示:
final class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
然後,在類別中插入下列的方法:
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
if response.actionIdentifier == "foodpin.makeReservation" {
print("Make reservation...")
if let phone = response.notification.request.content.userInfo["phone"] {
let telURL = "tel://\(phone)"
if let url = URL(string: telURL) {
if UIApplication.shared.canOpenURL(url) {
print("calling \(telURL)")
UIApplication.shared.open(url)
}
}
}
}
completionHandler()
}
當使用者選擇通知的動作時,userNotificationCenter(_:didReceive:withCompletionHandl er:) 會被呼叫,因此我們提供自己的實作來撥打電話給推薦的餐廳。
由於我們只需要處理「訂位」( Reserve a table )動作,因此我們先確認動作的識別碼, 然後從通知內容的userInfo 屬性取得餐廳的電話號碼。
在 iOS 中,你可以使用特定的 URL 來啟動某些系統 App。在本例中,我們想要開啟電話 App 來撥打號碼,你可以使用tel 協定來發起呼叫。以下是一個範例URL:
tel://<phone-number>
在上列的程式碼中,我們建立 telURL,然後呼叫 UIApplication 的 open 方法來啟動電話App。
在該方法的最後面,需要呼叫 completionHandler 區塊,來讓系統知道你已經完成通知的處理。
你要做的最後一件事是設定通知中心的委派。在 AppDelegate 的application(_:didFinish LaunchingWithOptions:) 中加入下列的程式碼:
UNUserNotificationCenter.current().delegate = self
這樣就完成了,執行專案,並在實機上部署 App 來進行測試,你必須使用實機,因為無法使用模擬器撥打電話。當你點選「Reserve a table」按鈕,App 會啟動 FoodPin App, 並向你顯示「撥打電話」的選項。
使用者通知框架是開發者管理及排程使用者通知的重要工具。本章中,我介紹了這個框架的概貌,並示範如何排程本地推播通知。
透過結合互動式及豐富內容的通知後,你可以提升使用者體驗,並讓你的 App 更加吸引人。這個作法能有效提高App 的留存率,當你開發下一個 App 時,請考慮利用使用者通知,並思考如何提升你的 App 價值。
最後,謝謝閱讀本書。這是一段很長的旅程。我期望你一切順利,能夠很快發布你的 App。若是你的 App 已通過審核,我很樂意聽到你的成功故事。歡迎隨時 Email 至 simonng@appcoda.com,並記得加入我們的臉書開發者社群: https://www.facebook.com/groups/appcodatw 。
本章所準備的範例檔中,有最後完整的 Xcode 專案可供你下載參考:http://www.appcoda.com/resources/swift59/swiftui-foodpin-usernotifications.zip 。