现在 iOS 9 可以用 Spotlight 搜索应用内的一些数据了,本章我们来看看增强版的 App Search。
App search in iOS 9 包含 三 部分
在 iOS 8 中,我们使用 NSUserActivity 来延续 activity 从一台设备到另一台设备上。在 iOS 9 上这个特性同样用来增强搜索,理论上讲,如果一个任务可以被表现为一个 NSUserActivity 传递给不同设备,那么也可以被存储在 search index 中,稍后用在同一台设备中。这样可以让你索引 App 的 activities,states 和 navigation poits,允许用户稍后通过 Spotlight 直接来找到这些内容。
比如一个旅行 app 可能会索引用户查看过的 hotels,或者一个新闻 app 会索引用户浏览过的新闻
最常规的 APP Search 就是 Core Spotlight ,iOS 自带的如股票应用,邮件、备忘录都可以通过他来索引,下面我们将让 App 更多的内容可以被索引
你可以将 Core Spotlight 看做是搜索信息的数据库,他提供了更细致的操作,比如哪些内容可以被索引。你可以索引各种格式的内容,从 videos 到 messages,包含更新和移除的
Core Spotlight 是搜索你应用内私有数据的最好方式
专门针对那种 app 数据来自网站,比如 Amazon,你可以搜索数百万的产品。使用开放标准的 web 内容标记,你可以将其显示在 Spotlight,Safari search results,或者在 app 中生成深链接
现在来看一个简单的应用 Colleagues,类似于一个雇员通讯录,我们下面使其可以在 Spotlight 中搜索应用内容
大概浏览下整个工程:
EmployeeService.swift和数据库打交道(这里是解析本地这个 json)提供一些查询、获取雇员等操作。这里还有两个 TODO Method,稍后我们会实现:
extension EmployeeService { public func indexAllEmployees() { // TODO: Implement this } public func destroyEmployeeIndexing() { // TODO: Implement this } }
工程加入了一个 Settings.bundle,允许我们在 iOS 系统中进行 App 的相关设置
先来看下用 NSUserActivity 实现 App search,选择 NSUserActivity 的理由:
创建一个 new file
EmployeeSearch.swift 在下面扩展了 Employee,主要添加了 userActivity
import CoreSpotlight extension Employee { // 用来标识 NSUserActivity 类型 public static let domainIdentifier = "com.raywenderlich.colleagues.employee" // 这个字典为你的 NSUserActivity 提供一个属性,用来标识 activity public var userActivityUserInfo: [NSObject: AnyObject] { return ["id": objectId] } public var userActivity: NSUserActivity { let activity = NSUserActivity(activityType: Employee.domainIdentifier) activity.title = name activity.userInfo = userActivityUserInfo activity.keywords = [email, department] return activity } }
挑主要的属性来说说:
下面在 EmployeeViewController.swift 中设置 employee 的 userActivity 属性,这样每次点开一个 employee,都会记录在 NSUserActivity 里,至于具体的记录细节要根据 Setting 的设定来做决定
let activity = employee.userActivity switch Setting.searchIndexingPreference { case .Disabled: activity.eligibleForSearch = false case .ViewedRecords: activity.eligibleForSearch = true activity.contentAttributeSet?.relatedUniqueIdentifier = nil case .AllRecords: activity.eligibleForSearch = true } userActivity = activity
eligibleForSearch 表示 activity 是否被加入设备的索引中。第二个的 .ViewedRecords
中的 relatedUniqueIdentifier
被设为 nil 是因为并没有对应的 Core Spotlight 来索引他,稍后在 Core Spotlight 章节中会重新设置
userActivity = activity
表明要设置当前 EmployeeViewController 实例的 userActivity
属性为这个配置好的 activity。
ViewController 的 userActivity
属性其实继承自 UIResponder
最后,重写同样是继承自 UIResponder
的方法,确保当你选中 search 结果时能够得到必要的信息:
override func updateUserActivityState(activity: NSUserActivity){ activity.addUserInfoEntriesFromDictionary(employee.userActivityUserInfo) }
在 UIResponder 的生命周期里,系统会多次调用该方法,你所要做的就是保持 activity 始终为最新状态。在这种情况下,你只需要简单地提供包含 employee's objectId
的字典 employee.userActivityUserInfo
文档中说该方法是用来让子类更新指定的 user activity ,通常使用 addUserInfoEntriesFromDictionary:
方法来添加一个 state 信息(表示 user's activity)到当前 activity 对象中,其实就是从给定的字典中获取信息然后添加到 activity 的 userInfo 字典中。传递给 userInfo 的信息尽可能地要小,否则需要更多的时间来恢复。经过实际测试,系统索引也是需要时间的。
然后,当 state 发生变化时,你需要设置 NSUserActivity 的 needsSave 为 YES,然后 updateUserActivityState:
方法会在合适的时候被调用
在 iOS 系统设置下,将 Colleagues 的索引设置为 Viewed Records ,然后运行 App,在 List 主界面中选择 Brent Reid ,按 Home 键退出,在系统搜索栏中输入 Brent Reid ,Bingo!出来结果了
现在搜索结果有点单调,NSUserActivity 提供了一个叫做 contentAttributeSet 的集合属性来允许你描述要显示的内容
你已经设置了 title,下面来补充 thumbnailData, supportsPhoneCall, contentDescription
还是在 EmployeeSearch.swift 下面增加一个计算属性 attributeSet
public var attributeSet: CSSearchableItemAttributeSet { // kUTTypeContact 表示联系人信息 let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeContact as String) attributeSet.title = name attributeSet.contentDescription = "/(department), /(title)/n/(phone)" attributeSet.thumbnailData = UIImageJPEGRepresentation(loadPicture(), 0.9) // 为了让电话按钮显示,你必须设置 supportsPhoneCall 为 true 然后提供一个电话号码 attributeSet.supportsPhoneCall = true attributeSet.phoneNumbers = [phone] attributeSet.emailAddresses = [email] attributeSet.keywords = skills return attributeSet }
有了这些细节, Core Spotlight 将会索引每一个然后显示在搜索结果中,也意味着现在可以通过:名字,部门,电话号码,甚至技能来搜索职员了。
不过现在还不能点击搜索结果跳转到相应的职员界面,我们下面来实现
前面已经为 NSUserActivity
实例设置了 activityType 和 userInfo 对象,打开 AppDelegate.swift 加入下面方法,告诉 delegate 相关的 data 要持续可用。当用户选择搜索结果时,该方法会被调用:
func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]?) -> Void) -> Bool { // 验证然后找出 objectId guard userActivity.activityType == Employee.domainIdentifier, let objectId = userActivity.userInfo?["id"] as? String else { return false } // 设置 employeeViewController 然后压入 navigationController if let nav = window?.rootViewController as? UINavigationController, listVC = nav.viewControllers.first as? EmployeeListViewController, employee = EmployeeService().employeeWithObjectId(objectId) { nav.popToRootViewControllerAnimated(false) let employeeViewController = listVC.storyboard? .instantiateViewControllerWithIdentifier("EmployeeView") as! EmployeeViewController employeeViewController.employee = employee nav.pushViewController(employeeViewController, animated: false) return true } return false }
现在点击搜索结果可以直接跳转到相应的职员页面了。
前面我们先学习 NSUserActivity,只是因为简单,现在我们使用 Core Spotlight 来索引全部数据。
在 EmployeeSearch.swift 中添加:
attributeSet.relatedUniqueIdentifier = objectId
这条命令会在 NSUserActivity 和 Core Spotlight 索引对象之间建立某种联系,如果你不做这一步,搜索时会得到重复的结果。
接着创建 CSSearchableItem
对象,表示 Core Spotlight 将要索引的对象:
var searchableItem: CSSearchableItem { let item = CSSearchableItem(uniqueIdentifier: objectId, domainIdentifier: Employee.domainIdentifier, attributeSet: attributeSet) return item }
因为我们之前就已经设置了 attributeSet,所以这里就很简单了。
打开 EmployeeService.swift 并 import CoreSpotlight
,现在我们来实现 indexAllEmployees()
public func indexAllEmployees() { // 1. 从数据库中提取所有的 employee 存放到 employees 数组中 let employees = fetchEmployees() // 2. map 遍历为 [CSSearchableItem] 数组 let searchableItems = employees.map { $0.searchableItem } CSSearchableIndex.defaultSearchableIndex() // 3. 使用 Core Spotlight 的默认索引,将 searchableItems 数组添加到索引中 .indexSearchableItems(searchableItems) { error in // 4. completionHandler if let error = error { print("Error indexing employees: /(error)") } else { print("Employees indexed.") } } }
至此,已经可以所以全部记录了,在设置中将 Indexing 切换为 All Records,运行,搜索:
和之前用 NSUserActivity 遇到的问题一样,点击搜索结果并不能跳转到对应的页面,还是到 AppDelegate.swift 里来修正一下:
import CoreSpotlight func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]?) -> Void) -> Bool { let objectId: String // 如果是由 NSUserActivity 索引的,activityType 应该是 reverse-DNS if userActivity.activityType == Employee.domainIdentifier, let activityObjectId = userActivity.userInfo?["id"] as? String { objectId = activityObjectId // 如果是 Core Spotlight 索引的,activityType 是 CSSearchableItemActionType } else if userActivity.activityType == CSSearchableItemActionType, let activityObjectId = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String { objectId = activityObjectId } else { return false } if let nav = window?.rootViewController as? UINavigationController, listVC = nav.viewControllers.first as? EmployeeListViewController, employee = EmployeeService().employeeWithObjectId(objectId) { nav.popToRootViewControllerAnimated(false) let employeeViewController = listVC.storyboard?.instantiateViewControllerWithIdentifier("EmployeeView") as! EmployeeViewController employeeViewController.employee = employee nav.pushViewController(employeeViewController, animated: false) return false } return true }
如果职员被老板开除了,我们删除职员信息的同时记得也要删除索引,这里可以简单的删除所有索引:
CSSearchableIndex .defaultSearchableIndex() .deleteAllSearchableItemsWithCompletionHandler { error in if let error = error { print("Error deleting searching employee items: /(error)") } else { print("Employees indexing deleted.") } }
去系统设置里,将 Indexing 设置为 Disabled,运行观察下效果
除了全部删除我们还可以按组删除和按具体的 item 删除:
deleteSearchableItemsWithDomainIdentifiers(_:completionHandler:)
deleteSearchableItemsWithIdentifiers(_:completionHandler:)
最后要注意是保持 indexes 更新,使用之前介绍的方法来更新要索引的 items
indexSearchableItems(_:completionHandler:)
默认的所有 Core Spotlight 索引的内容都是私有的,但你可以标记 NSUserActivity 的属性 eligibleForPublicIndexing
为 true,将其设置为 public。这样就能使内容成为 Apple cloud 索引的一部分。
另外一种使内容公开索引的方式是使用 web 标记,下章介绍。
Core Spotlight框架也提供了几个高级特性
Core Spotlight app extension 可以在程序不运行的情况下也能对索引进行维护
Spotlight index extensions 包含一个 CSIndexExtensionRequestHandler 子类(遵循 CSSearchableIndexDelegate 协议)遵循两个方法:
searchableIndex(_: reindexAllSearchableItemsWithAcknowledgementHandler:)
searchableIndex(_: reindexSearchableItemsWithIdentifiers: acknowledgementHandler:)
Core Spotlight 也支持批量更新,这种情况不能使用 defaultSearchableIndex
,你需要创建你自己的 CSSearchableIndex 实例
beginIndexBatch()
fetchLastClientStateWithCompletionHandler()
CSSearchableItem
对象用来索引 indexSearchableItems(_:completionHandler:)
用来索引 endIndexBatchWithClientState()