2016 年 9 月 23 - 24 日,由 CSDN 和创新工场联合主办的“MDCC 2016 移动开发者大会• 中国”(Mobile Developer Conference China)将在北京• 国家会议中心召开,来自 iOS、Android、跨平台开发、产品设计、VR 开发、移动直播、人工智能、物联网、硬件开发、信息无障碍10个领域的技术专家将分享他们在各自行业的真知灼见。
目前,MDCC 大会门票正处于 8 折优惠票价阶段,五人以上团购更有每人减免 300元特惠,限量供应(票务详情链接,8 折优惠,欲购从速!)
作者简介: 于天航,知乎 iOS 团队负责人。毕业于北京交通大学,今日头条创始员工,后就职于百度。一直在 iOS 领域从事技术研发工作。
1983 年,一小部分开发者来到美国加州蒙特瑞(Monterey)见证了第一款苹果公司设计的个人电脑的诞生。有趣的是这场会议并不对外开放,甚至是完全保密的,苹果刻意避开了媒体和公众的视线,与会者甚至要签署严格的保密协议。官方意义上的 WWDC 开始于 1990 年,而对于国内早期的 iOS 开发者来说,对 WWDC 最早的记忆应该是从 2007 年第一代 iPhone 横空出世开始的,到今天已经走过了 9 个年头,iOS 系统的版本号也已经到了两位数。相比于 iOS 7 开始每个 iOS 版本大刀阔斧的革新,今年的 iOS 10 更加专注于对现有功能的改进,最大的亮点无疑是 SiriKit 对开发者开放,本文也着重介绍一下 SiriKit 的设计理念和实践。
Siri 是一款苹果 iOS 系统提供的智能语音助手软件,它的全名是 Speech Interpretation and Recognition Interface。用语音和 Siri 交互可以让你实现传送信息、安排会议、拨打电话、查看附近餐厅等功能,并且全程都是 Hand Free 的;你还可以让 Siri 学习你的声纹,然后通过“嘿 Siri”来激活它。
从 2011 年 Siri 第一次以 iOS 内置软件的形式随 iPhone 4s 一同问世之后,苹果一直致力于更新改进以使它更加智能,iOS 的开发者们一直期盼有一天自己的 App 也可以利用到 Siri 带来的好处和便利。终于在 WWDC 2016 上,苹果开放了 Siri 的 API,开发者们可以利用SiriKit将自己的服务提供给用户。苹果为了保护iOS的用户体验,在API的开放上一向非常谨慎,SiriKit也不例外。利用SiriKit开发者目前只能做如下六件事情:
SiriKit 将上述六种行为描述为六种类型的意图(Intents),提供对应的Intent.framework
和用于UI展示的IntentUI.framework
两个工具包方便开发者实现上述功能。
如果你的应用刚好在这些领域之内,那么恭喜你,可以使用 SiriKit 为应用增加系统级的入口、提升用户体验,并且能够在地图这样的系统应用中使用你的服务(WWDC 两个 Siri 的 Session 中也都提到了这一点),无论你的应用进程是在后台运行,还是已经被 Kill。
为便于描述,本文以一款名为“知了”的 SiriKitDemo 为例进行讲解,“知了”App 是一款发送消息意图类型的 SiriKit App。
先来看下面一段对话:
你:用知了给 Jon Snow 发一条消息。
Siri:消息的内容是?
你:Winter is coming。
这是 Siri 消息类型意图的典型使用场景。然而用户的行为总是不可确定的,在发送一条消息给 Snow 的时候你也可能会这样说:
你:用知了发送一条消息。
Siri:给谁发?
你:Jon Snow。
Siri:发送的内容是?
你:Winter is coming。
当然还有支付、打车等场景,为了适应各种复杂的交互场景,SiriKit 将每种意图的处理总结为图 1 描述的过程。
从图1中不难看出以下特点。
Action:App 在后台收到这个 Intent 之后开始针对这个 Intent、通过 Siri 向用户进行必要的信息采集、加工、处理,并完成用户的意图。每种意图下采集的信息不尽相同,发送消息需要采集的信息如下:
Response:用户意图处理完成后,Siri 将处理结果(成功或失败)展现给用户。
SiriKit 将底层的深度学习、CNN(卷积神经网络)等技术封装起来,开发者只需要关注自己如何提供服务,如何满足用户需求。因此 SiriKit 的实现代码大部分是围绕用户意图(Intent)来完成的,在前文所述的过程中,每个意图的生命周期可分成图 2 描述的三个阶段。
从图2中,我们可以看出以下特点。
Confirm:这一阶段主要实现以下两件事情。
检查完成意图所需要的必要状态,比如:
Siri 会根据当前信息决定是否展示给用户一个确认窗口,比如付款时的确认窗口或者消息的发送窗口。Confirm 阶段也会出现多次,根据实际经验经验每次Resolve方法调用后都会调用一次 Confirm 方法。
和通知中心插件、Watch 扩展一样,SiriKit 也是通过 Extensions 的方式与主 App 结合起来。SiriKit 提供了两个新的扩展:
同时,为了让 App 更好地支持特定意图的语义识别,App 可以在自定义词表中提供一些 App 相关的术语和短句,这一点在后面的小节会有详细说明。
在 Xcode 8 中点击 TARGETS 界面左下角的加号新建一个 Target,可以看到新版本的 Xcode 中苹果为我们提供了很多新的 extension 模板,选择 Intents
Extension(如图 3 所示)。
点击下一步,填写 Product Name,选择语言版本。注意,这里有个“Include UI Extension”的选项,勾选之后会自动创建一个“Intents UI Extension”的 Target 用于 Siri 中的界面展示(如图4所示)。
点击完成。Xcode 8 会生成两个 Target 和对应文件夹(如图5所示)。
在 Capabilities 中打开 Siri 的开关,如果使用 Xcode 8 并且 App 已经设置为自动管理 App Signing(Xcode 8 的又一重大革新),Xcode 会自动更新相应的证书文件,完成后如图 6 所示。
图7显示了修改.entitlements文件添加Siri支持。
Intents Extension Target 创建后默认生成了两个文件:
编辑 Intents Extension 的 Info.plist 文件,在 NSExtension 字典中指定 App 支持的意图类型。以消息类型意图为例 Intents Extension 的 Info.plist 如图 8 所示。
这里解释一下 plist 中的关键字。
com.apple.intents-service
;$(PRODUCT_MODULE_NAME).IntentHandler
。打开 IntentHandler.swift 文件,部分默认实现如图 9 所示。
我们先来运行测试一下。
在 return self 一行打上断点,然后选择 Intents Extension 对应的 Target,在 iOS 10 设备上运行。此时会弹出一个 iPhone App 列表,选择 Siri,编译运行后会自动打开Siri(如图10所示)。
测试 Demo 中主 App 的名字为“知了”,在 iPhone 上打开的 Siri 界面输入语音“使用知了发送一条消息”,Siri 会在这条语音中识别出用户意图为使用“知了”发送消息,将语音信息转换为文本转发给 Demo App,程序会停留在我们刚才设置的断点上。
SiriKit 对中文的支持效果并不理想,例如语音输入“用知了发送一条消息”,会将“一条消息”识别为发送的内容;再比如输入“用知了发消息给宏昌”,会将“发消息给宏昌”识别为发送的内容。
经过多次测试,官方 Session 中示例中动词化应用名字的方式识别效果最好。因此在 SiriKit 的中文语音测试中推荐下面形式。
你:知了发信息给宏昌。
Siri:你想对宏昌说什么。
你:看《冰与火之歌》了吗。
Siri:好的。可以发送了吗?
你:发送。
停止运行,接下来来看一下具体的编码实现。为了当支持多重 Intents 时的代码看起来简洁,我们把不同类型的 Intent 转发到不同的类。新建SendMessageIntentHandler
类。
class SendMessageIntentHandler: NSObject,INSendMessageIntentHandling
在IntentHandler.swift
中更新handler(for intent:)
方法,在Intent为INSendMessageIntent
时返回它。
override func handler(for intent: INIntent) -> AnyObject {
// This is the default implementation. If you want different objects to handle different intents,
// you can override this and return the handler you want for that particular intent.
if intent is INSendMessageIntent {
return SendMessageIntentHandler()
}
return self
}
具体的业务逻辑在SendMessageIntentHandler
类实现。
resolve 是 Intent 处理逻辑的第一个阶段。消息发送类型的意图处理过程中,需要 resolve 的信息有下面两个。
resolveContent(forSendMessage intent:, with completion:)
。 intent.recipients
、消息内容intent.content等。completion方法,用于返回当前的处理结果。根据处理结果的不同 SiriKit 设计了7个回调方法:
如何合理的使用上述回调方法呢?举例来说,消息接受者的 resolve 过程中可能发生的情况有三种:
.success(with:recipientMatched)
;.disambiguation(with:disambiguationOptions)
;.unsupported()
。注意: 一条消息的接收者可能有多个,因此消息接受者处理中
completion
方法的参数是一个内容类型为INPersonResolutionResult
的数组,而不是单一结果。
消息内容的处理则比较简单,判断输入内容是否合法后在completion
方法中输入INStringResolutionResult
类型的结果并调用。
以下是 resolve 阶段完整的代码实现:
// 1.1 Resolve: 处理联系人信息
func resolveRecipients(forSendMessage intent: INSendMessageIntent, with completion: ([INPersonResolutionResult]) -> Void) {
if let recipients = intent.recipients {
var resolutionResults = [INPersonResolutionResult]()
for recipient in recipients {
let matchingContacts = contacts(personName: recipient.displayName)
switch matchingContacts.count {
case 2 ... Int.max:
let disambiguationOptions: [INPerson] = matchingContacts.map { contact in
return contact
}
resolutionResults += [.disambiguation(with: disambiguationOptions)]
case 1:
let recipientMatched = matchingContacts[0]
resolutionResults += [.success(with: recipientMatched)]
case 0:
resolutionResults += [.unsupported()]
default:
break
}
}
completion(resolutionResults)
} else {
completion([INPersonResolutionResult.needsValue()])
}
}
// 1.2 Resovle: 处理消息内容信息
func resolveContent(forSendMessage intent: INSendMessageIntent, with completion: (INStringResolutionResult) -> Void) {
if let text = intent.content where !text.isEmpty {
completion(INStringResolutionResult.success(with: text))
} else {
completion(INStringResolutionResult.needsValue())
}
}
每个 resolve 方法调用后都会调用 confirm 方法,Demo 中只检查了当前的登录状态,当然你也可以在 confirm 中检查更多的状态,如下所示:
// 2. Confirm:
func confirm(sendMessage intent: INSendMessageIntent, completion: (INSendMessageIntentResponse) -> Void) {
// 登录后才能发消息
if hasValidAuthentication() {
completion(INSendMessageIntentResponse(code: .success, userActivity: nil))
} else {
let userActivity = NSUserActivity(activityType: String(INSendMessageIntent.self))
userActivity.userInfo = [NSString(string: "error"):NSString(string: "UserLoggedOut")]
completion(INSendMessageIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity))
}
}
confirm 的参数仍然是intent
和completion
,completion
方法的参数类型为INSendMessageIntentResponse
。在应用逻辑中,检查到用户未登录时,response 的 code 会传入failureRequiringAppLaunch
,即需要启动 Demo 应用完成后续的登录操作。SiriKit 提供了如下 code:
值得注意的是除了 code 外,response 还接受另一个参数userActivity
,它是一个NSUserActivity
类型的对象,主要用于在主 App 启动时将当前环境上下文传递给主 App。
多次交互确认发送消息所需要的信息之后,Siri 会询问我们“要发送吗?”。语音输入“好的”、“发送”或“确定”都可以触发发送。此时 SiriKit 会调用handle(sendMessage intent:, completion:)
方法,消息发送的处理逻辑都应该在这个回调方法中完成。
// 3. Handle:
func handle(sendMessage intent: INSendMessageIntent, completion: (INSendMessageIntentResponse) -> Void) {
if intent.recipients != nil && intent.content != nil {
let success = sendMessage(target: intent.recipients, content: intent.content)
completion(INSendMessageIntentResponse(code: success ? .success : .failure, userActivity: nil))
} else {
completion(INSendMessageIntentResponse(code: .failure, userActivity: nil))
}
}
处理的结果通过INSendMessageIntentResponse
返回给 SiriKit。
至此,SiriKit 的接入工作就完成了,但用户看到的是 SiriKit 提供的默认 UI,为了提供更好的用户体验,使界面风格契合 App 的特点,SiriKit 提供了自定义UI界面的扩展——Intents UI Extension。
一个 Intents UI Extension 的完整生命周期如图 11 所示。
在上面的小节中我们已经创建过 Intents UI Extension 的 Target,默认包含三个文件:
基础视图的实现
和 Intents Extension 一样,我们需要修改 Intents UI Extension 的 Info.plist 中NSExtension
部分以支持INSendMessageIntent
(如图12所示)。
NSExtension
部分
下面解释一下 plist 中的 key。
com.apple.intents-ui-service
。我们还是先来运行一次看看效果。在 Storyboard 中添加一个文本内容为“Winter is coming”的 UILabel,再次运行,效果如图 13 所示。
Intents UI的实现
视图的逻辑编码在IntentViewController
类中,IntentViewController
是UIViewController
的子类,因此你可以使用所有 UIKit 的 API。也就是说,在这个类中编写界面和我们之前开发过的ViewController
编写方式基本一致。
那么 Intents Extension 在处理 Intent 过程中所发生的状态变化是如何传递给 Intents UI 的呢?先来看一下IntentViewController
类的基本实现:
class IntentViewController: UIViewController, INUIHostedViewControlling, INUIHostedViewSiriProviding {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - INUIHostedViewControlling
// Prepare your view controller for the interaction to handle.
func configure(with interaction: INInteraction!, context: INUIHostedViewContext, completion: ((CGSize) -> Void)!) {
// Do configuration here, including preparing views and calculating a desired size for presentation.
if let completion = completion {
completion(self.desiredSize)
}
}
var desiredSize: CGSize {
return self.extensionContext!.hostedViewMaximumAllowedSize
}
var displaysMessage: Bool {
return true
}
}
IntentViewController
类继承于UIViewController
之外,还实现了INUIHostedViewControlling
协议的configure
方法。
configure(with interaction:, context:,
completion:
这个方法的第一个参数是当前这次 Siri 交互上下文信息,包括 intent 和 intentResponse 等属性,UI 层可以根据上下文信息完成本次视图的更新展现(如:展示加载动画),并返回一个 CGSize 给 Siri 来指明目前视图的大小。NSExtensionContext
中给出了视图大小的最小和最大允许值,返回的 CGSize 只能在两者之间。
var desiredSize: CGSize {
return self.extensionContext!.
hostedViewMaximumAllowedSize
}
每次 Siri 和 App 交互后都会顺序调用viewDidLoad()
方法和configure()
方法。另外,值得注意的是 Intents UI Extension 的界面区域内是不支持手势交互操作的。
INUIHostedViewSiriProviding
从上文的截图可以看到,除了你的自定义 UI 区域,Siri 还提供了一个默认区域展示消息的接收者和内容,如果我们在自定义区域已经展示了这些信息,这个默认区域会导致信息重复。SiriKit 为此提供了INUIHostedViewSiriProviding
这个协议,通过实现代理方法可以隐藏这个区域:
var displaysMessage: Bool {
return true
}
隐藏后效果如下:
官方的 Session 中简单介绍了 Siri 识别当前的语义的一些依据:
SiriKit 处理的四个过程里面,App 的处理逻辑主要是从 Intent 过程开始的。Speech 过程由 Siri 独立完成,考虑到每个 App 都有自己独特的方式和用户交流,因此在语义识别的过程中,Siri 允许应用提供自定义的词表(User Vocabulary)用来帮助 Siri 更好地识别和 App 相关的语境(如图15所示)。
通过 App 相关的自定义词表(术语或短句)提供给 Siri,让 Siri 可以识别出和 App 相关的独特信息可以优化使用体验。这样的关键词表分为两类:
提供 App-Specific Vocabulary 的方式是在主 App 内集成 App vocabulary plist 文件。在主 App(注意不是 Extension)的 Bundle 中创建AppIntentVocabulary.plist
文件来帮助 Siri 了解和 App 有关的术语,甚至可以提供该术语的发音信息和使用例句。
AppIntentVocabulary.plist
文件是支持本地化的,可以为 App 的每种语言版本提供不同的词表信息。
这个 plist 文件的根字典可以包含两个 Key,如表1所示。
Key | 类型 | 简介 |
---|---|---|
ParameterVocabularies | 字典列表 | (必填)符合 App 目前支持的 Intents 某个特定属性的术语列表 |
IntentPhrases | 字典列表 | (选填)在 Siri 的引导中展示并提供给 Siri 做深度学习的短句。 |
ParameterVocabularies列表中的字典可以包含两个Key,如表2所示。
Key | 类型 | 简介 |
---|---|---|
ParameterNames | 字符串列表 | (必填)术语可以应用于的 Intent 属性,用.的形式表示 |
ParameterVocabulary | 字典列表 | (必填)术语的同义词列表,包括发音和例句 |
ParameterNames 中目前只能为打车和健身类型的意图设置 App 级别的关键词表。
ParameterVocabulary 列表中的字典包含以下两个 Key。
AppIntentVocabulary.plist
的所有本地化版本中应该是一致的。VocabularyItemSynonyms:一组同义词的字典列表。该字典可以包含三个Key:
IntentPhrase 列表中的每个字典都包含两个 key,如表 3 所示。
Key | 类型 | 简介 |
---|---|---|
IntentName | 字符串 | (必填) 例句所对应的 Intent 类(6种) |
IntentExamples | 字符串列表 | (必填) 当前 Intent 语境中用户可能的表达方式 |
以“呼呼打车”中打车场景为例,IntentExamples 可能的表达方式有:
图16展示了一个支持 SiriKit 的健身应用中 AppIntentVocabulary.plist 的完整示例。
另一种向 Siri 提供自定义词表的方式是在运行时由 App 提供一个用户特定的、以OrderedSet
类型返回的关键词表,比如通讯录中的收藏、最近联系人。
在提供自定义词表时应注意以下几点:
使用INVocabulary
对象为一个独立用户注册一个指定Intent
类型的词表。通过调用 SiritKit 提供的 API:-setVocabularyStrings:ofType:方法来实现。
以提供最近联系为例,代码实现如下:
//SiriKit
NSOrderedSet *tStrings = [self recentlyContacts];
INVocabulary *voc = [INVocabulary sharedVocabulary];
[voc setVocabularyStrings:tStrings ofType:INVocabularyStringTypeContactName];
// --SiriKit
对于注册方法的使用有以下几点提示。
到这里你已经了解了利用 SiriKit 开发 App 和 Extensions 的全部过程和相关知识。当然,和地理位置、通知一样,使用 SiriKit 也需要请求系统权限,并且在弹窗中解释用户的哪些信息可能被发送到Siri。
Siri 从问世开始,开发者们就一直在等待 API 开放,SiriKit 如今总算是“千呼万唤始出来”。SiriKit 不仅为 App 带来了“人工智能”,同时也为每个应用提供了新的系统级入口,用户无需进入 App,也不需要任何点击操作就可以完成想要的操作,消息发送、拨打视频电话、转账、打车、健身变得更加即时方便。
当然,SiriKit 和所有首次开放的 API 一样,目前仍然不够成熟,有两个方面需要提高:一是更智能丰富的中文语意识别;二是由于 Siri 的输入存在非常多的可能性,在实际应用的过程中会出现令人困惑,或API调用结果不符合我们预期的情况。
无论如何,通过 WWDC 2016 可以看到,iOS、macOS、watchOS和tvOS这四个产品线上的布局已经完成,苹果生态整合又前进了一步。各个平台的交互体验、一致性、安全性都得到了进一步的提升。正如 SiriKit 的开放一样,苹果开发者们面临新的挑战的同时,也应该看到许多新的机会。随着 Siri 的发展,用户与智能设备的交互方式不仅仅停留在视觉和触觉,越来越多的人会开始使用 Siri,感受 Siri 给这个世界带来的不同。
关于移动开发新技术,更多精彩尽在MDCC 2016,详情请查看大会官网:MDCC 2016移动开发者大会。