
作為你的第一個 App,FoodPin App 已經相當令人印象深刻了,但是如果你想要進一步增強它,並納入 iOS 裝置提供的一些現代技術,則千萬不要錯過接下來的內容。
自從 iPhone 6s 與 6s Plus 推出以來,Apple 向我們介紹一種與手機互動的全新方式,稱為「3D 觸控」( 3D Touch ),此功能為使用者介面增加一個新維度,並提供獨特的使用者體驗。透過 3D 觸控,iPhone 不僅可以偵測到你的觸碰( Touch ),還可以感知到你對顯示器施加的壓力力道。
自 iPhone 11、iPhone 11 Pro 與 iPhone 11 Pro Max 開始,Apple 已將所有 iPhone 型號上的 3D 觸控替換為觸覺觸控( Haptic Touch )。儘管觸覺觸控和 3D 觸控有相似之處,但它們之間還是存在明顯的差異,3D 觸控依賴壓力觸控( Force Touch ),而觸覺觸控則是透過「長按」(Touch and hold )手勢來啟動。
你是否嘗試過用更大的壓力按主畫面上的 App 圖示呢?當你這樣做時,它會顯示一組快速動作,讓你直接訪問 App 的特定部分,這是觸覺觸控的實際應用範例,稱為「快速動作」( Quick Action )。

在本章中,我將介紹如何在 SwiftUI 專案中實作快速動作,並建立自訂的 URL 類型來處理這些快速動作,如圖 28.1 所示:
Apple 提供兩種類型的快速動作:「靜態」( Static )與「動態」( Dynamic )。靜態快速動作寫死在 Info.plist 檔,當使用者安裝 App 後,即使在首次啟動 App 之前,也可使用快速動作。
顧名思義,動態快速動作本質上是動態的,App 在執行期中建立並更新快速動作。以 Instagram App 為例,其快速動作顯示了切換帳戶的選項,它們必須是動態的,因為帳戶名稱會隨時變更。

但是,它們有一個共同點是無論你使用靜態還是動態快速動作,你最多可以建立四個快速動作。
要建立靜態快速動作非常簡單,你只需要編輯 Info.plist 檔,並新增 UIApplicationShortcut Items 陣列。陣列的每個元素都是一個包含下列屬性的字典:
UIApplicationShortcutItemType (必填) - 用於識別快速動作的唯一識別碼,這個識別碼在所有的 App 中應是唯一的,因此較好的作法是將識別碼的前綴加上 App Bundle ID (例如:com.appcoda.UIApplicationShortcutItemTitle (必填) - 使用者可見的快速動作名稱。 UIApplicationShortcutItemSubtitle (optional) - 快速動作的副標題,它是顯示在快速動作標題下方的可選型別字串。UIApplicationShortcutItemIconType (選填) - 用於指定系統庫中圖示類型的可選型別 字串。你可以參考 這份文件 ,來了解可用的圖示類型。UIApplicationShortcutItemIconFile (選填) - 如果你想使用自己的圖示,則從 App Bundle 中指定要使用的圖示圖片,或者在素材目錄中指定圖片名稱,圖示必須要是矩形且單色,尺寸為 35×35(1x)、70×70(2x) 與105×105(3x)。UIApplicationShortcutItemUserInfo (選填) - 包含你想要傳送的一些額外資訊的可選型別字典,例如:這個字典的其中一種用途是傳送 App 版本。如果你想加入一些靜態快速動作,下列是使用 UIApplicationShortcutItems 陣列來建立「New Restaurant」捷徑的範例,如圖 28.3 所示。

現在你應該對靜態快速動作有些概念了,接下來我們來說明動態快速動作。我們將修改 FoodPin 專案來示範,並加入三個快速動作至 App 中:
首先,為何我們要使用動態快速動作呢?簡單的答案是「我想要介紹如何使用動態快速動作」,不過實際的原因是「我只想在使用者看完導覽畫面後才啟用這些快速動作」。
要編寫程式碼來建立快速動作時,你只需要使用所需屬性來實例化一個 UIApplication ShortcutItem 物件,然後將它指定給 UIApplication 的 shortcutItems 屬性。以下是一個例子:
let shortcutItem = UIApplicationShortcutItem(type: "com.appcoda.NewRestaurant", localizedTitle: "New Restaurant", localizedSubtitle: nil, icon: UIApplicationShortcutIcon(type: .add), userInfo: nil)
UIApplication.shared.shortcutItems = [shortcutItem]
第一行程式碼定義一個具有快速動作類型 com.appcoda.NewRestaurant 和系統圖示 .add 的捷徑項目,快速動作的標題設定為「New Restaurant」;第二行程式碼使用 shortcutItem 初始化一個陣列,並將其設定為 shortcutItems 屬性。
要建立快速動作,我們開啟 FoodPinApp.swift,並在 FoodPinApp 結構中建立一個輔助方法:
func createQuickActions() {
if let bundleIdentifier = Bundle.main.bundleIdentifier {
let shortcutItem1 = UIApplicationShortcutItem(type: "\(bundleIdentifier).OpenFavorites", localizedTitle: "Show Favorites", localizedSubtitle: nil, icon: UIApplicationShortcutIcon(systemImageName: "tag"), userInfo: nil)
let shortcutItem2 = UIApplicationShortcutItem(type: "\(bundleIdentifier).OpenDiscover", localizedTitle: "Discover Restaurants", localizedSubtitle: nil, icon: UIApplicationShortcutIcon(systemImageName: "eyes"), userInfo: nil)
let shortcutItem3 = UIApplicationShortcutItem(type: "\(bundleIdentifier).NewRestaurant", localizedTitle: "New Restaurant", localizedSubtitle: nil, icon: UIApplicationShortcutIcon(type: .add), userInfo: nil)
UIApplication.shared.shortcutItems = [shortcutItem1, shortcutItem2, shortcutItem3]
}
}
程式碼和前面的範例幾乎一樣。我們建立了三個快速動作項目,每個項目都有自己的識別碼、標題與圖示。
那麼,我們應該在何處呼叫這個 createQuickActions() 方法呢?
根據 Apple 的說法,當 App 轉換至背景狀態時,是更新任何動態快速動作的最好時機, 因為系統會在使用者返回主畫面之前執行程式碼。
下一個問題是我們如何偵測這種狀態變化呢? 在 SwiftUI 中, 你可以透過觀察 Environment 中的 scenePhase 值來讀取目前的場景階段。在 FoodPinApp 中插入下列程式碼來取得 scenePhase 值:
@Environment(\.scenePhase) var scenePhase
你可以將 .onChange 修飾器加到 WindowGroup 來監看場景階段的變化,如下所示:
WindowGroup {
MainView()
}
.modelContainer(for: Restaurant.self)
.onChange(of: scenePhase) { oldValue, newValue in
switch newValue {
case .active:
print("Active")
case .inactive:
print("Inactive")
case .background:
createQuickActions()
@unknown default:
print("Default scene phase")
}
}
上列的程式碼監聽 scenePhase 狀態的變化。當狀態變更為 background 階段時,我們執行createQuickActions 方法來更新快速動作。
現在你可以在模擬器中建立與執行 App。App 啟動後返回主畫面,在 FoodPin 圖示上長按1 秒鐘,然後你應該會看到快速動作選單,如圖 28.4 所示。

快速動作還沒有準備好運作,因為我們還沒有實作啟動快速動作所需的方法。在 UIKit 中,有一個定義在UIWindowSceneDelegate 協定的方法,稱為「windowScene(_:performActionFor:completionHandler:)」,當使用者選擇快速動作時,此方法會被呼叫,我們將在 FoodPinApp.swift 中實作該方法。
Note: 目前版本的 SwiftUI不提供處理快速動作的原生方法,這便是為何我們仍然需要回復改用 UIKit。 .
那麼,當選擇快速動作時,我們如何導引使用者到 App 的特定部分呢?我們將使用自訂 URL 協定,這個功能提供一種參照 App 中特定內容或資源的方式,例如:我們定義了以下的自訂 URL:
foodpinapp://actions/OpenDiscover
當使用者點擊 URL 時,系統導引使用者至 FoodPin App 的「Discover」標籤。而我們要做的是建立一個名為「foodpinapp://」的自訂 URL 協定,每個快速動作都有自己的自訂 URL 來導引使用者至 App 的特定畫面:
foodpinapp://actions/OpenFavorites - 開啟「Favorites」標籤。 foodpinapp://actions/OpenDiscover - 開啟「Discover」標籤。 foodpinapp://actions/NewRestaurant - 顯示「New Restaurant」視圖。 在 FoodPinApp.swift 中插入下列的程式碼片段:
final class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
@Environment(\.openURL) private var openURL: OpenURLAction
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
completionHandler(handleQuickAction(shortcutItem: shortcutItem))
}
private func handleQuickAction(shortcutItem: UIApplicationShortcutItem) -> Bool {
let shortcutType = shortcutItem.type
guard let shortcutIdentifier = shortcutType.components(separatedBy: ".").last else {
return false
}
guard let url = URL(string: "foodpinapp://actions/" + shortcutIdentifier) else {
print("Failed to initiate the url")
return false
}
openURL(url)
return true
}
}
上列的程式碼建立了一個MainSceneDelegate 類別,並實作 windowScene(_:performActi onFor:completionHandler:) 方法來啟動快速動作。handleQuickActions 方法將捷徑項目作為參數。
它首先解析其 type 值(例如:com.appcoda.foodpin.OpenFavorites)來分析這個捷徑項目是什麼,根據提取的值來建構自訂URL(例如:foodpinapp://actions/OpenFavorites )。
要在 SwiftUI 中開啟 URL,你可以從環境中取得 .openURL 鍵並開啟 URL,如下所示:
openURL(url)
接下來,我們需要建立一個 AppDelegate 類別來設定 MainSceneDelegate 為委派類別。在同一個檔案中插入下列的程式碼:
final class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
let configuration = UISceneConfiguration(name: "Main Scene", sessionRole: connectingSceneSession.role)
configuration.delegateClass = MainSceneDelegate.self
return configuration
}
}
儘管我們已經建立了 AppDelegate 類別並採用了所有必需的協定,但是 SwiftUI 中的 FoodPinApp 結構對於我們所實作的所有類別仍然一無所知,我們需要讓它知道使用 UIKit 中的 AppDelegate 類別。你可以在 FoodPinApp 結構中插入下列這行程式碼,來註冊 AppDelegate 類別:
@UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
現在你應該了解我們如何處理快速動作,不過這個快速動作還沒有完成,如果你嘗試開啟任何一個快速動作,你應該會在主控台中看到以下的錯誤訊息:
2023-10-26 12:39:31.547378+0800 FoodPin[36298:10370760] [default] Failed to open URL foodpinapp://actions/OpenDiscover: Error Domain=NSOSStatusErrorDomain Code=-10814 "(null)" UserInfo={_LSLine=229, _LSFunction=-[_LSDOpenClient openURL:options:completionHandler:]}
原因是我們在專案中註冊了自訂協定,如果沒有註冊,系統將不會重新導引 URL 到你的 App。
切換到專案設定,並開啟「Info」標籤。在「URL Types」區塊中,點選「+」按鈕來加入新的URL 類型,接著輸入你的識別碼,並設定「URL Schemes」選項為「foodpinapp」, 如圖 28.5 所示。

我們完成了嗎?我們建立了 URL 協定,App 會在選擇快速動作時開啟 URL,但還有一件事未完成,即 App 仍然不知道如何回應這個 URL。
現在切換至 MainView.swift,將 onOpenURL 修飾器加到 TabView:
.onOpenURL(perform: { url in
switch url.path {
case "/OpenFavorites": selectedTabIndex = 0
case "/OpenDiscover": selectedTabIndex = 1
case "/NewRestaurant": selectedTabIndex = 0
default: return
}
})
當視圖收到 URL 時,會呼叫 .onOpenURL 函數,然後我們檢查 URL 路徑,並相應更新 selectedTabIndex 的值。
對於 NewRestaurant 動作, 我們需要加入一個特別處理。開啟 RestaurantListView. swift,並將另一個 onOpenURL 修飾器加到 NavigationStack:
.onOpenURL(perform: { url in
switch url.path {
case "/NewRestaurant": showNewRestaurant = true
default: return
}
})
當偵側到 NewRestaurant 動作時,我們設定 showNewRestaurant 變數為「true」。
這個 App 現在已經準備好進行測試了。在模擬器中執行它,並試著在主畫面中開啟快速動作,App 應該會依據你選擇的快速動作來導引你到相應的畫面。
我猜你的所有測試都是在 App 執行時完成的,你是否嘗試過關閉 App 並再次啟動快速動作呢?如果 App 沒有執行,則快速動作就無法運作。windowScene(_:performActionFor: completionHandler:) 方法只有在 App 處於執行狀態時,才會被呼叫。
如果 App 沒有執行,系統會呼叫下列的委派方法:
scene(_:willConnectTo:options:)
要修正此問題,則開啟 FoodPinApp.swift,並在 MainSceneDelegate 類別實作該方法:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let shortcutItem = connectionOptions.shortcutItem else {
return
}
handleQuickAction(shortcutItem: shortcutItem)
}
你可以再次測試 App,即使 App 沒有執行,快速動作還是能夠運作。
在本章中,我介紹了一些在 SwiftUI 專案中實作快速動作的基本 API。快速動作可作為 App 的捷徑,讓使用者方便訪問特定功能。作為一個 App 開發者,優先考量提供良好的使用者體驗是很重要的,當你要開發自己的 App 時,可考慮結合快速動作來提升整體的使用者體驗,並讓使用者能夠快速且容易地訪問主要功能。
在本章所準備的範例檔中,有最後完整的 Xcode 專案可供你參考 http://www.appcoda.com/resources/swift59/swiftui-foodpin-haptictouch.zip 。