iOS 10 by Tutorials 筆記(六)

分類:技術 時間:2016-10-25

繼續沒事翻翻書,做做筆記,因為整本書都還在 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 中,比如定義氣球的五種狀態:

  1. moving 移動狀態
  2. waitingForRequest 等待請求
  3. withPassengers 接上乘客
  4. enRouteToCollection 接乘客的路上
  5. 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(付費的開發者帳號才有這一選項)

再總結下所有步驟:

  1. 添加一個 Intents extension
  2. 創建相關 handler objects
  3. 在 INExtension 的子類中返回第二步創建的 handler objects
  4. 在 extension 的 Info.plist 里聲明支持的 intents 類型(可以有多個)
  5. 向用戶請求使用 Siri 的權限
  6. 在主程序的 Info.plist 中添加向用戶請求權限時的說明
  7. 添加 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 的數據模型。

其實就是我們通過設置 INRequestRideIntentResponserideStatus 屬性,來讓 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
  } 
}

模型都轉換完畢,現在來重新實現一下 RideRequestHandlerhandle(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
  }

最后運行一下看下效果

-EOF-

Tags: iOS開發

文章來源:https://chengwey.com/ios-10-by-tutorials-bi-ji-liu


ads
ads

相關文章
ads

相關文章

ad