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 中搜索应用内容

大概浏览下整个工程:

  • 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 的相关设置

三、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!出来结果了

2、Adding more information to search results

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

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

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,运行,搜索:

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()

-EOF-

如果感觉此文对你有帮助,请随意打赏支持作者 😘

chengway

认清生活真相之后依然热爱它!

Subscribe to Talk is cheap, Show me the world!

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!