繼續沒事翻翻書,做做筆記,因為整本書都還在 Early Access 的狀態,出來哪章寫哪章,稍后再調整順序吧。
Chapter 6: SiriKit
蘋果從 iOS 5 開始就提供了 Siri 這個功能,經過這么多年的改進,終于在 iOS 10 上開放了部分 API,這樣我們自己開發的應用也能獲得 Siri 的支持了。
應該對 Siri 的支持是通過 app extension 實現的,而且調試需要有付費的開發者帳號和真機,模擬器是不能調試 Siri 的。
在開始本章任務前,需要了解一下 Siri 的工作原理,Siri 在底層使用了一組 domains,每個 domain 都代表了一種相關功能(比如 Messaging)
也可以說每個 domain 都是一組意圖/目的(intents)的集合,意圖說白了就是我們使用 siri 具體要完成的任務。比如 Messaging domain 就是一種用來發送信息、搜索信息,設置信息屬性的 intent 。
每一種 intent 都是 INIntent
的子類,而且它都配備相應的 handler protocl 和一個 INIntentResponse
子類用來和 SiriKit 交互。
所以在應用中的 SiriKit 對語言處理可以轉變為意圖的判斷,然后開發者的代碼來檢查意圖是否明晰合理,是否能夠完成,如果可以就去執行任務。
關于 SiriKit 支持 Intents 的種類,可以去查看官方文檔
Would you like to ride in my beautiful balloon?
本章我們要開發一個熱氣球應用,里面的氣球運行的邏輯已經提前寫好了,核心代碼被放在一個單獨的 FrameWork 中,這樣不管是主程序還是 extension 都能很方便地訪問
核心代碼過了一遍,寫得很不錯,雖然不是本章必須,但還是記錄下思想
- WenderLoonSimulator 類用來集中協調所有資源的使用
- Models
- Driver.swift 司機
- Balloon.swift 氣球相關的一些屬性和方法
- Motion.swift 運行的核心邏輯
運行的核心邏輯都封裝在 Motion.swift 中,比如定義氣球的五種狀態:
- moving 移動狀態
- waitingForRequest 等待請求
- withPassengers 接上乘客
- enRouteToCollection 接乘客的路上
- completed 完成
還定義了兩種類 HoldingPattern 和 Journey 分別對應了待命模式和旅行模式,這兩種模式下分別根據當前狀態和目標狀態進行一系列的更新。具體代碼可以自行去研究下,寫的很好。
真機上跑一下,可以看到熱氣球分散在倫敦的上空,并且還是實時隨機移動著。
現在開始增加對 Siri 的支持,選擇 File\New\Target 下的 iOS/Application Extension/Intents Extension ,不要勾選 Include UI Extension
這樣創建了一個新的 target 和一組文件(Info.plist 和 IntentHandler.swift),在工程中找到 RideRequestExtension Group 下的 IntentHandler.swift ,把里面的樣本代碼刪干凈
import Intents class IntentHandler: INExtension { }
INExtension是 Intents extension 的入口,它只做一件事情:為 intent 提供相應的 handle object
既然如此,我們可以理解為每個 intent 都有一個相關的 handle protocl 來處理意圖,為了邏輯清晰,我們新建了一個 RideRequestHandler.swift 來實現相關的 handle protocl:
import Intents class RideRequestHandler: NSObject, INRequestRideIntentHandling { }
INRequestRideIntentHandling協議用來處理打車請求的 intent ,它有一個必須實現的方法
func handle(requestRide intent: INRequestRideIntent, completion: @escaping (INRequestRideIntentResponse) -gt; Void) { let response = INRequestRideIntentResponse( code: .failureRequiringAppLaunchNoServiceInArea, userActivity: .none) completion(response) }
我們姑且先返回一個失敗的請求
回到 IntentHandler.swift,因為之前說過,這是一個入口文件,能處理很多類型的 Intent,因此需要先判斷 Intent 的類型
override func handler(for intent: INIntent) -gt; Any? { if intent is INRequestRideIntent { return RideRequestHandler() } return .none }
除此之外還需在 Info.plist 設置,這樣 Siri 才能只知道你的 App 有能力處理這種意圖 Intent,打開 RideRequestExtension 中的 Info.list 做如下設置
下一步是請求用戶授權,來到主程序的 AppDelegate.swift 中,添加鑒權代碼
import UIKit import Intents @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -gt; Bool { requestAuthorisation() return true } fileprivate func requestAuthorisation() { INPreferences.requestSiriAuthorization { status in if status == .authorized { print(quot;Hey, Siri!quot;) } else { print(quot;Nay, Siri!quot;) } } } }
打開 主程序組 里 Info.list ,添加 Privacy - Siri Usage Description ,顯示給用戶的描述按照需求寫就好 最后一步在工程 target 的 Capabilities 中打開 Siri(付費的開發者帳號才有這一選項)
再總結下所有步驟:
- 添加一個 Intents extension
- 創建相關 handler objects
- 在 INExtension 的子類中返回第二步創建的 handler objects
- 在 extension 的 Info.plist 里聲明支持的 intents 類型(可以有多個)
- 向用戶請求使用 Siri 的權限
- 在主程序的 Info.plist 中添加向用戶請求權限時的說明
- 添加 Siri entitlement 到應用(開啟 Siri Capabilities)
此時選擇主程序 WenderLoon Scheme 運行,就會彈出鑒權窗口,點擊 OK
切到 RideRequestExtension Scheme 運行,彈出的窗口選擇 Siri 應用,Siri 就會啟動,我們就能對著 Siri 說話了。嘗試說一句:quot;Book a ride using WenderLoon from Heathrow airportquot;
如果你發音標準,Siri 是可以理解的,不過現在還沒有提供服務(還記得之前設置的 INRequestRideIntentResponse
嗎)
99 (passengers in) red balloons
處理一個 intent 意圖分三步,第一步確認所有必需的信息是否提供,如果有缺失,Siri 會再次詢問用戶。對于打車請求,可以有以下信息:
- 接乘客的地點
- 目的地
- 乘客數量
- 乘車選項
- 付款方式
所有的這些參數都來自于 INRequestRideIntentHandling 協議中的相關方法,你可以從協議方法的參數中獲取你感興趣的信息,然后通過 completion block 傳入相關參數與 Siri 進行交流。每一類信息都對應了一個協議方法。
Siri 處理的整個流程是這樣的
既然每一種信息都對應了一個協議方法,我們先來實現『乘客乘車點』的協議,打開 RideRequestHandler.swift 添加如下方法:
func resolvePickupLocation(forRequestRide intent: INRequestRideIntent, with completion: @escaping (INPlacemarkResolutionResult) -gt; Void) { if let pickup = intent.pickupLocation { completion(.success(with: pickup)) } else { completion(.needsValue()) } }
如果第一次沒有告訴 Siri 乘車點,Siri 會讓我們補充更多的信息
這里要注意,每次與 Siri 的交互都會啟動單獨的 handler object (RideRequestHandler),所以在處理 intents 時不能使用狀態信息。
返回 Xcode 接著添加目的地的 protocol
func resolveDropOffLocation(forRequestRide intent: INRequestRideIntent, with completion: @escaping (INPlacemarkResolutionResult) -gt; Void) { if let dropOff = intent.dropOffLocation { completion(.success(with: dropOff)) } else { completion(.notRequired()) } }
接下來要處理的 PartySize,即從 Siri 獲取到乘客數量,然后判斷我們的熱氣球能否坐得下。這個判斷的邏輯需要最開始提到的 WenderLoonSimulator 類來實現。
我們在 IntentHandler 中初始化一個 WenderLoonSimulator 的實例,然后作為初始化參數傳入 RideRequestHandler
import Intents import WenderLoonCore class IntentHandler: INExtension { let simulator = WenderLoonSimulator(renderer: nil) override func handler(for intent: INIntent) -gt; Any? { if intent is INRequestRideIntent { return RideRequestHandler(simulator: simulator) } return .none } }
為此添加下 RideRequestHandler 的初始化方法
let simulator: WenderLoonSimulator init(simulator: WenderLoonSimulator) { self.simulator = simulator super.init() }
現在我們就能用來處理乘客數量的邏輯了,從協議參數中獲取乘客數量,然后與載客量比較,最后將結果反饋給 completion block
func resolvePartySize(forRequestRide intent: INRequestRideIntent, with completion: @escaping (INIntegerResolutionResult) -gt; Void) { switch intent.partySize { case .none: completion(.needsValue()) case let .some(p) where simulator.checkNumberOfPassengers(p): completion(.success(with: p)) default: completion(.unsupported()) } }
我們氣球可以坐四個人,你對著 Siri 說有 12 個乘客,返回如下結果
最后一個協議方法 confirm 當所有參數信息都滿足時調用
func confirm(requestRide intent: INRequestRideIntent, completion: @escaping (INRequestRideIntentResponse) -gt; Void) { let responseCode: INRequestRideIntentResponseCode if let location = intent.pickupLocation?.location, simulator.pickupWithinRange(location) { responseCode = .ready } else { responseCode = .failureRequiringAppLaunchNoServiceInArea } let response = INRequestRideIntentResponse(code: responseCode, userActivity: nil) completion(response) }
在其中,我們判斷了乘客附近一定距離內有沒有可供搭乘的熱氣球
以上僅僅是處理一個 intent 意圖的前兩步(索取必需的信息,確認信息),這一切完成后,最后一步才是真正接受了這個請求,根據提供的信息來處理請求。
You can’t handle the truth
還記得最初我們在 RideRequestHandler 里的 handle 方法中只返回了一個失敗的 code 嗎?類似于說本地區不提供服務。現在我們可以根據 Siri 提供的意圖信息來完成實際工作。
當用戶看到確認對話框并請求乘坐熱氣球后,Siri 會顯示另一個對話框展示預訂的細節。不同類型的 intent 所展示的對話框細節不同,展示這種細節需要 intent 自己的數據模型,而且每一種類型的 intent 都有自己的數據模型子集,所以你要把原有的數據模型轉換成 Intent 的數據模型。
其實就是我們通過設置 INRequestRideIntentResponse 的 rideStatus 屬性,來讓 Siri 向用戶展示預訂細節,而設置 rideStatus 屬性需要 intent 自己的數據模型。
切換到 WenderLoonCore 框架,添加新模型 IntentsModels.swift
import Intents // 1 public extension UIImage { public var inImage: INImage { return INImage(imageData: UIImagePNGRepresentation(self)!) } } // 2 public extension Driver { public var rideIntentDriver: INRideDriver { return INRideDriver( personHandle: INPersonHandle(value: name, type: .unknown), nameComponents: .none, displayName: name, image: picture.inImage, rating: rating.toString, phoneNumber: .none) } }
上面代碼創建了 Intent 版本的 image 和 driver
不幸的是并沒有一種 INBalloon 類型,Intents framework 提供了 INRideVehicle,我們可以拿來模擬一下熱氣球
public extension Balloon { public var rideIntentVehicle: INRideVehicle { let vehicle = INRideVehicle() vehicle.location = location vehicle.manufacturer = quot;Hot Air Balloonquot; vehicle.registrationPlate = quot;B4LL 00Nquot; vehicle.mapAnnotationImage = image.inImage return vehicle } }
模型都轉換完畢,現在來重新實現一下 RideRequestHandler 的 handle(intent:completion:)
方法
// 1 理論上不提供乘車點到不了這步,但是以防萬一, Siri 嘛你懂得 guard let pickup = intent.pickupLocation?.location else { let response = INRequestRideIntentResponse(code: .failure, userActivity: .none) completion(response) return } // 2 雖然不提供目的地熱氣球就隨機跑,但是 balloon simulator 還是需要的 let dropoff = intent.dropOffLocation?.location ?? pickup.randomPointWithin(radius: 10_000) // 3 用來封裝有關交通工具的信息 let response: INRequestRideIntentResponse // 4 檢測附近是否有氣球可用 if let balloon = simulator.requestRide(pickup: pickup, dropoff: dropoff) { // 5 INRideStatus 包含了乘坐工具的一些細節信息,我們設置好了通過 INRequestRideIntentResponse 返回給用戶 let status = INRideStatus() status.rideIdentifier = balloon.driver.name status.phase = .confirmed status.vehicle = balloon.rideIntentVehicle status.driver = balloon.driver.rideIntentDriver status.estimatedPickupDate = balloon.etaAtNextDestination status.pickupLocation = intent.pickupLocation status.dropOffLocation = intent.dropOffLocation response = INRequestRideIntentResponse(code: .success, userActivity: .none) response.rideStatus = status } else { response = INRequestRideIntentResponse(code: .failureRequiringAppLaunchNoServiceInAr ea, userActivity: .none) } completion(response)
status.rideIdentifier 應該是唯一的,這里我們使用了司機名,實際工程中可以自行設置
運行一下
Making a balloon animal, er, UI
我們也可以為 Siri 的展示窗口自定義的 UI,你需要添加另一個 extension,這一次在 File\New\Target 中選擇 Intents UI Extension ,命名為 LoonUIExtension
這次的 extension 包含一個 View Controller,一個 Storyboard,以及一個 Info.plist
同樣打開 Info.plist ,修改 NSExtension/NSExtensionAttributes/IntentsSupported 數組的元素項為 INRequestRideIntent
打開 Storyboard,設置 UI 如下
然后在 View Controller 中設置好 IBOutlet,該 VC 遵循了 INUIHostedViewControlling 協議,提供了一個 configure(with: context: completion:)
方法,用于設置 VC 內容是調用,所以我們在此方法中來配置我們的界面元素
// Prepare your view controller for the interaction to handle. func configure(with interaction: INInteraction!, context: INUIHostedViewContext, completion: ((CGSize) -gt; Void)!) { // Do configuration here, including preparing views and calculating a desired size for presentation. guard let response = interaction.intentResponse as? INRequestRideIntentResponse else { driverImageView.image = nil balloonImageView.image = nil subtitleLabel.text = quot;quot; completion(self.desiredSize) return } // 這個 extension 會被調用兩遍,一次是請求,一次是確認請求, // 第二次應該會有司機信息 if let driver = response.rideStatus?.driver { let name = driver.displayName driverImageView.image = WenderLoonSimulator.imageForDriver(name: name) balloonImageView.image = WenderLoonSimulator.imageForBallon(driverName: name) subtitleLabel.text = quot;\(name) will arrive soon!quot; } else { driverImageView.image = nil balloonImageView.image = nil subtitleLabel.text = quot;Preparing...quot; } // 傳入了最大允許的尺寸 completion(self.desiredSize) } var desiredSize: CGSize { return self.extensionContext!.hostedViewMaximumAllowedSize }
最后運行一下看下效果
Tags: iOS開發
文章來源:https://chengwey.com/ios-10-by-tutorials-bi-ji-liu