转载

iOS 9 by Tutorials 笔记(二)

Chapter 2: Introducing App Search

现在 iOS 9 可以用 Spotlight 搜索应用内的一些数据了,本章我们来看看增强版的 App Search。

一、App search APIs

App search in iOS 9 包含 三 部分

  • NSUserActivity

    在 iOS 8 中,我们使用 NSUserActivity 来延续 activity 从一台设备到另一台设备上。在 iOS 9 上这个特性同样用来增强搜索,理论上讲,如果一个任务可以被表现为一个 NSUserActivity 传递给不同设备,那么也可以被存储在 search index 中,稍后用在同一台设备中。这样可以让你索引 App 的 activities,states 和 navigation poits,允许用户稍后通过 Spotlight 直接来找到这些内容。

    比如一个旅行 app 可能会索引用户查看过的 hotels,或者一个新闻 app 会索引用户浏览过的新闻

  • Core Spotlight

    最常规的 APP Search 就是 Core Spotlight ,iOS 自带的如股票应用,邮件、备忘录都可以通过他来索引,下面我们将让 App 更多的内容可以被索引

    你可以将 Core Spotlight 看做是搜索信息的数据库,他提供了更细致的操作,比如哪些内容可以被索引。你可以索引各种格式的内容,从 videos 到 messages,包含更新和移除的

    Core Spotlight 是搜索你应用内私有数据的最好方式

  • Web markup

    专门针对那种 app 数据来自网站,比如 Amazon,你可以搜索数百万的产品。使用开放标准的 web 内容标记,你可以将其显示在 Spotlight,Safari search results,或者在 app 中生成深链接

二、Getting started

现在来看一个简单的应用 Colleagues,类似于一个雇员通讯录,我们下面使其可以在 Spotlight 中搜索应用内容

iOS 9 by Tutorials 笔记(二)

大概浏览下整个工程:

  • Employee.swift 是雇员 model,数据来自一个本地的 json
  • EmployeeService.swift和数据库打交道(这里是解析本地这个 json)提供一些查询、获取雇员等操作。这里还有两个 TODO Method,稍后我们会实现:

    extension EmployeeService {   public func indexAllEmployees() {   // TODO: Implement this  }    public func destroyEmployeeIndexing() {   // TODO: Implement this   } }
  • 工程加入了一个 Settings.bundle,允许我们在 iOS 系统中进行 App 的相关设置

    iOS 9 by Tutorials 笔记(二)

三、Searching previously viewed records

先来看下用 NSUserActivity 实现 App search,选择 NSUserActivity 的理由:

  • 它很简单,创建一个实例,设置几个属性。
  • 当你使用 NSUserActivity 来标记 用户活动 (user activities),iOS 会为频繁访问的内容进行评级,这样搜索出来的结果也会区分优先级
  • 如果需要支持 Handoff 只需一步之遥

1、Implement 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  } } 

挑主要的属性来说说:

  • activityType: 稍后你会用这个标记 NSUserActivity 实例
  • title: 作为搜索结果的名字显示
  • userInfo: 该字典用来存储需要传递内容,比如说存储搜索结果,然后点按搜索结果跳转到 app 相应的内容。
  • keywords: 一组本地化的关键字,方便用户搜索时找到记录

下面在 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!出来结果了

iOS 9 by Tutorials 笔记(二)

2、Adding more information to search results

现在搜索结果有点单调,NSUserActivity 提供了一个叫做 contentAttributeSet 的集合属性来允许你描述要显示的内容

iOS 9 by Tutorials 笔记(二)

你已经设置了 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 将会索引每一个然后显示在搜索结果中,也意味着现在可以通过:名字,部门,电话号码,甚至技能来搜索职员了。

iOS 9 by Tutorials 笔记(二)

不过现在还不能点击搜索结果跳转到相应的职员界面,我们下面来实现

3、Opening search results

前面已经为 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 } 

现在点击搜索结果可以直接跳转到相应的职员页面了。

四、Indexing with Core Spotlight

前面我们先学习 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.swiftimport 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,运行,搜索:

iOS 9 by Tutorials 笔记(二)

Make the results do something

和之前用 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   } 

Deleting items from the search index

如果职员被老板开除了,我们删除职员信息的同时记得也要删除索引,这里可以简单的删除所有索引:

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:)
  • 按具体 item 删除 deleteSearchableItemsWithIdentifiers(_:completionHandler:)

最后要注意是保持 indexes 更新,使用之前介绍的方法来更新要索引的 items

indexSearchableItems(_:completionHandler:)

五、Private vs. public indexing

默认的所有 Core Spotlight 索引的内容都是私有的,但你可以标记 NSUserActivity 的属性 eligibleForPublicIndexing 为 true,将其设置为 public。这样就能使内容成为 Apple cloud 索引的一部分。

另外一种使内容公开索引的方式是使用 web 标记,下章介绍。

六、Advanced features

Core Spotlight框架也提供了几个高级特性

1、Core Spotlight App Extensions

Core Spotlight app extension 可以在程序不运行的情况下也能对索引进行维护

Spotlight index extensions 包含一个 CSIndexExtensionRequestHandler 子类(遵循 CSSearchableIndexDelegate 协议)遵循两个方法:

  • searchableIndex(_: reindexAllSearchableItemsWithAcknowledgementHandler:)
  • searchableIndex(_: reindexSearchableItemsWithIdentifiers: acknowledgementHandler:)

2、Batch indexing

Core Spotlight 也支持批量更新,这种情况不能使用 defaultSearchableIndex ,你需要创建你自己的 CSSearchableIndex 实例

  1. 创建 CSSearchableIndex.
  2. 标记开始更新 beginIndexBatch()
  3. 获取最近更新 fetchLastClientStateWithCompletionHandler()
  4. 准备下一次更新的 CSSearchableItem 对象用来索引
  5. 使用 indexSearchableItems(_:completionHandler:) 用来索引
  6. 结束更新 endIndexBatchWithClientState()
正文到此结束
Loading...