如何使用iOS9中的Core Spotlight框架

作者: Gabriel Theodoropoulos,时间:2015/12/22
翻译:BigNerdCoding, 如有错误欢迎指出。原文链接

伴随这每一个iOS新版本的发布,苹果公司都会为全球的开发者带来新的“好东西”,以及对于原有功能的持续改进。是在最新的版本iOS9中,苹果不仅依旧保留了这个传统,再次为我们提供了新的框架和API接口。利用这些新特性,开发者可以将他们的应用程序提高到一个更高的水平上。Core Spotlight框架就是这其中之一,该框架包含了很多等待我们去发现和使用的强大API接口。

Core Spotlight框架是被称为Search APIs这个大集合API中的的一部分。该框架为程序员提供了一个机会来增加他们应用程序可发现性、可见性以及访问的便捷性,并且作为新特性该框架无法在之前版本的iOS中使用的。Search APIs让用户和应用之间的关系变得更加密切,前者可以以更新更快的方法来使用后者,同时后者也可以立即响应前者。在iOS9中除了Core Spotlight还有其它一些搜索功能,包括(仅供参考):

  1. NSUserActivity类里面的新方法和属性(该类负责存储应用的状态并用于后面状态的恢复)。

  2. 是网页内容能在设备中进行搜索的网页标记(web markup

  3. 让程序从网页内容中的链接直接启动的通用链接(universal links

我们不会对上面所有都进行讲解,但是我们会对Core Spotlight框架进行仔细的分析和讲解。但在此之前,我们需要对Core Spotlight有个初步的了解。

clipboard.png

Core Spotlight框架使得应用程序的数据能够Spotlight中被搜索查询,系统会将与之相关的数据以及其它结果一起返回。第一次用户可以查询到除苹果自家应用以外其它第三方应用数据并与之交互,这让人影响深刻且意义重大。这里所说的与第三方应用相关的数据进行交互的意思是:不仅仅是当我们点击搜索结果应用程序会自动启动,还有开发者可以给予用户权利去为Spotlight中选择的数据选择最合适的视图控制器。

从开发者角度来说,集成Core Spotlight框架并使用其中的API接口并不是意见复杂的事情。就像你在这篇教程后面发现的一些,它仅仅需要几行代码而已。这里的关键是开发者需要向系统查询他们应用程序数据的索引,而这些索引在之前必须已经定义描述好了。

由于这篇教程本身就是专门关于Core Spotlight框架内容的,我就不再对概念进行更细致的介绍了。如果你对于如何将其应用于实践实现一些东西(我觉得这才是真正有趣的地方),那么就继续往下阅读吧。我相信在你读完这篇教程之后,你会对让应用支持Spotlight是如此简单的一件事而感到满意与开心。

关于Demo

与往常一样,我们通过一个Demo应用来深挖我们今天话题的一些具体细节。在该Demo中我们会加入一些数据到应用中,这些数据能够在设备或者模拟器的Spotlight中被搜索到。虽然应用的大概是这些,但是还是有必要对一些细节进行说明一下。

我们的演示应用的目的是展示一些电影以及与之相关的信息,例如:摘要、导演、明星、评价等等。所有的这些电影数据都会在一个tableview展示出来,当用户选择了某个电影的时候会跳转到详细介绍的页面视图中。没有更进一步的操作了,这个功能和数据已经足以让我们理解 Core Spotlight接口是如何工作的了。至于应用数据的获取,你可以去国际电影数据库(IMDB)去查找;演示应用中数据也是我从里面找到的。

你可以通过下面演示动画的流程和效果一睹为快该应用。

clipboard.png

在这个教程里主要要实现两个目标:首先最重要的是让应用里的数据能够在Spotlight里面搜索到。通过这样做,当用户通过使用关键值进行搜索时与之相关的结果将展示出来。设置这些关键值是后面需要做的工作的一部分,定义它们也是我们的职责之一。

当用户点击搜索出来的电影时应用将被触发启动,并带出我们的第二个目标。当应用启动后,如果此时用户不采取任何动作的话,默认的视图控制器将会加载一个包含电影列表的tableview并将其显示给用户。然后,如果从用户体验方面考虑的话,这样做并不是很好。一个更理想的情况是,我们能够展示出Spotlight搜索出来结果的详细信息,这也是最终我们所做的。总之,我们不仅需要能够让电影能被搜索到,还要在用户点击搜索结果时能够展示详细介绍。下面的这个演示能够更加清楚表达出这两个目标:

clipboard.png

为了能现在就能开始工作,你可以下载开始工程。在工程里面你可以发现:

  • UI部分已经完成了,同时IBOutlet属性也设置好了。

  • 实现了最小化的tableview

  • 所以的电影数据都存在于.plist文件。另外,这里还有五个图像与之对应。

如果你想知道列表文件中包含的每一个电影的数据,下面会展示一个截图来说明一切:

clipboard.png

在了解Core Spotlight接口的一些细节之前,我们先要实现下面两个任务:

  1. 我们会加载并填充电影数据到tableview

  2. 我们需要将电影数据并在视图控制器里展示详细信息

虽然可以让我们更快的接近这个话题的要点,但是我并没有在上面的工程里面实现这两个任务时因为一个简单的原因:我坚信通过演示应用和样本数据操作的过程,会让你对这些具体数据是如何变的可以被Spotlight搜索到的理解更加简单直接。不需要担心,所有的前期工作很少,很快就可以完成。

加载并显示实例数据

假设你已经下载了初始项目并见过电影数据的列表文件,接下来我们开始工作。在MoviesData.plist中你可以发现五个与IMDB网站上随机选择数据对应的条目。我第一个目标是加载.plist文件中的数据到一个数组中,并在tableview中展示。

首先直接打开ViewController.swift文件,并在文件的头部直接声明如下属性:

var moviesInfo: NSMutableArray!

所有电影都将被加载到钙数组中,每个电影都使用dictionary中的健值和值与之进行匹配。

接下来我们写一个小的自定义功能,该功能将实现数据加载。正如接下来看到的一样,我只是确认该文件是否真实存在,如果存在的话,我们就使用文件的内容初始化数组:

func loadMoviesInfo() {
    if let path = NSBundle.mainBundle().pathForResource("MoviesData", ofType: "plist") {
        moviesInfo = NSMutableArray(contentsOfFile: path)
    }
}

我们将会在viewDidLoad()中调用上面的函数。但是你确保该函数在configureTableView()函数之前被调用,就像下面代码一样:

erride func viewDidLoad() {
    super.viewDidLoad()

    // Load the movies data from the file.
    loadMoviesInfo()

    configureTableView()
    navigationItem.title = "Movies"
}

请注意,你也可以不用自定义一个函数来完成文件的加载。但是作为有代码对齐强迫症的我来说,封装到一个函数是一个更好点的方法,即使是对于这么简单的功能。

在确定所有的电影数据已经在应用启动时候就已经全部加载后,我们可以开始来修改tableview的实现,以实现电影数据的展示。这里所需要做的事情并不是很多:我们根据电影的的数量来定义行数,然后我们在tableview cell中正确的显示出来。

显然,行数应该和电影的数量是一样的。但是我们首先不能忘了必须确保数据确实存在,否则应用加载一个不存在的数据的时候会导致崩溃。

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if moviesInfo != nil {
        return moviesInfo.count
    }

    return 0
}

下面,就该轮到将数据展示出来了。为了最终的演示,你能在起始工程里面找到一个继承于UITableViewCellMovieSummaryCell的子类,还有一个于只对应的.xib文件。

clipboard.png

cell会展示一个图像,题目,以及部分介绍和评价。所有的UI控件都与IBOutlet属性进行了关联,这些属性名称你可以在MovieSummaryCell.swift文件中找到。

@IBOutlet weak var imgMovieImage: UIImageView!

@IBOutlet weak var lblTitle: UILabel!

@IBOutlet weak var lblDescription: UILabel!

@IBOutlet weak var lblRating: UILabel!

上面变量的名称以及表明了自己的目的,下面我们让它将与之相关的电影数据展示出来。我们回到ViewController.swift文件中,像下面这样更新一下函数里面的代码:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("idCellMovieSummary", forIndexPath: indexPath) as! MovieSummaryCell

    let currentMovieInfo = moviesInfo[indexPath.row] as! [String: String]

    cell.lblTitle.text = currentMovieInfo["Title"]!
    cell.lblDescription.text = currentMovieInfo["Description"]!
    cell.lblRating.text = currentMovieInfo["Rating"]!
    cell.imgMovieImage.image = UIImage(named: currentMovieInfo["Image"]!)

    return cell
}

上面使用的变量currentMovieInfo其实可以省略不写,但是有了这个变量会让代码书写变的容易一些。

现在你可以允许代码了,如何一切顺利的话,你可以看见一个带有电影信息的tableview列表。当目前为止,我们完成的工作相信大家早就熟悉了,所以就直接开始第二步吧:展示电影的详细信息。

显示详细数据

我们使用MovieDetailsViewController类来展示我们选中的电影的详细信息。Interface Builder中的各个场景已经存在了,接下来我们需要做两件事:首先,将ViewController中的详细数据传递过来,该数据来源于前面定义的那些UI控件中。

所以,我们在MovieDetailsViewController类的开始处也定义一个变量:

var movieInfo: [String: String]!

先回到ViewController.swift文件中去看看当用户点击某一行电影的时候我们需要做些什么。当事件发生的时候,我们需要知道是当前被点击数据的索引行号,接着我们就可以从电影数组中找出对应的数据并在idSegueShowMovieDetails触发界面切换时传递给下一个视图控制器。获得行号索引时比较容易的,但是我们需要一个自定义属性来存储它,因此我们在ViewController类中进行如下声明:

var selectedMovieIndex: Int!

接下来,我们就按照下面的方式来处理点击选中事件:

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    selectedMovieIndex = indexPath.row
    performSegueWithIdentifier("idSegueShowMovieDetails", sender: self)
}

在这里我们做了简单的两件事情:第一保存所选的行号到属性里面,然后触发界面切换事件到电影详细介绍页。

然后这里还缺少了东西,我们并没有获取相应的数据并将数据传递到MovieDetailsViewController类中。所以我们需要像下面这样重载prepareForSegue:sender:函数:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if let identifier = segue.identifier {
        if identifier == "idSegueShowMovieDetails" {
            let movieDetailsViewController = segue.destinationViewController as! MovieDetailsViewController
            movieDetailsViewController.movieInfo = moviesInfo[selectedMovieIndex] as! [String : String]
        }
    }
}

很简单对吧!我们通过seguedestinationViewController属性实现了对MovieDetailsViewController实例的访问,并将我们获得的数据赋值到了这部分开头声明的变量中。

现在,我们再次到开MovieDetailsViewController.swift文件,我们需要定义一个函数。在这个函数里面,我们需要将movieInfo变量中的值赋值到对应的UI控件中去,我们的任务到这里也就完成了。下面的代码很简单我就不进行讲解了:

func populateMovieInfo() {
    lblTitle.text = movieInfo["Title"]!
    lblCategory.text = movieInfo["Category"]!
    lblDescription.text = movieInfo["Description"]!
    lblDirector.text = movieInfo["Director"]!
    lblStars.text = movieInfo["Stars"]!
    lblRating.text = movieInfo["Rating"]!
    imgMovieImage.image = UIImage(named: movieInfo["Image"]!)
}

最后,我们在viewWillAppear函数里面调用上面的函数:


override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    
    lblRating.layer.cornerRadius = lblRating.frame.size.width/2
    lblRating.layer.masksToBounds = true
    
    if movieInfo != nil {
        populateMovieInfo()
    }
}

这一部分也完成了,你可以运行应用看看效果。

为Spotlight建立数据索引

使用iOS9中的Core Spotlight框架,可以让应用中的数据在Spotlight中被搜索到。而这么做的关键是通过Core SpotlightAPI获得数据的索引,这样它才能被搜索并展示给用户。但是无论是应用本身还是CS(Core Spotlight)都不能自己决定什么样的数据应该被搜索和展示。以特殊的形式将我们的数据提供给API接口是我们自己应该做的事情。

进一步来说:所有那些我们希望能被Spotlight搜索到的数据首先必须是一个CSSearchableItem对象,然后这些对象被放入一个数组中并将索引提供给API。一个CSSearchableItem对象里面包含了一个属性集。该属性集将这个对象的所有细节都提供给了iOS系统,例如什么样的数据应该在搜索的时候显示出来(像电影名称、图片、描述),什么关键字能让我们的应用出现在Spotlight中。一个单一的CSSearchableItem对象中的所有属性使用一个CSSearchableItemAttributeSet对象进行表示。该对象提供了很多的属性用于我们进行赋值。你可以查看链接进行进一步了解。

建立索引是这篇教程所需要做的最后一步,通常情况下有以下几个步骤:

  1. 为每一个数据设置数据,例如一个电影(CSSearchableItemAttributeSet对象)

  2. 使用第一步设置的属性来实例化一个可搜索对象(CSSearchableItem对象)。

  3. 将所有对象放入到一个数组中。

  4. Spotlight使用上面数组中的数据查询数据

我们按照上面的步骤一步步实现我们的目的,首先我们在ViewController.swift文件中定义一个函数setupSearchableContent()。在这部分的最后,你回发现让数据能够被搜索到其实并不是很难的一件事。当然,我们也不可能一步就实现这个目标,就像我无法一次就把所有的代码实现都给你一样;取而代之的是我把代码分散开来进行讲解,这样也有利于你消化吸收。不用担心也没多少东西。

在你编写自定义函数之前,首先你需要引入两个框架:

import CoreSpotlight
import MobileCoreServices

下面我们就来编写函数,并定义一个数组来存放可搜索对象:

func setupSearchableContent() {
    var searchableItems = [CSSearchableItem]()

}

我们在一个循环里面访问每一个电影:

func setupSearchableContent() {
    var searchableItems = [CSSearchableItem]()

    for i in 0...(moviesInfo.count - 1) {
        let movie = moviesInfo[i] as! [String: String]
    }
}

对于每一个电影,我们都创建一个CSSearchableItemAttributeSet对象来存储那些在Spotlight搜索到的时候需要在结果中显示的数据。在这个演示应用里面,我们将电影名称、电影描述、图片这些属性设置进去。

func setupSearchableContent() {
    var searchableItems = [CSSearchableItem]()

    for i in 0...(moviesInfo.count - 1) {
        let movie = moviesInfo[i] as! [String: String]

        let searchableItemAttributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeText as String)

        // Set the title.
        searchableItemAttributeSet.title = movie["Title"]!

        // Set the movie image.
        let imagePathParts = movie["Image"]!.componentsSeparatedByString(".")
        searchableItemAttributeSet.thumbnailURL = NSBundle.mainBundle().URLForResource(imagePathParts[0], withExtension: imagePathParts[1])

        // Set the description.
        searchableItemAttributeSet.contentDescription = movie["Description"]!
    }
}

注意上面的代码,我们是如何将电影图片设置为一个属性的。这里有两个方法能够实现:一个是使用图片的URL,或者使用图像的NSData对象。这里简单一点的方法就是提供每一个电影图片的URL,因为我们知道这些图片都在应用里面。但是这么做要求我们将图片名称划分为图片真是名称和图片类型拓展,所以我使用了String类中的componentsSeparatedByString方法。剩下的部分应该很好理解。

接下来就该轮到设置应用在Spotlight中搜索时的关键字了。关键字一定要提前想好,因为这决定很大程度上影响了你的应用被Spotlight和用户搜索到的可能。在演示应用中我们使用电影类型和明星来作为关键字。

func setupSearchableContent() {
    var searchableItems = [CSSearchableItem]()

    for i in 0...(moviesInfo.count - 1) {
        ...

        var keywords = [String]()
        let movieCategories = movie["Category"]!.componentsSeparatedByString(", ")
        for movieCategory in movieCategories {
            keywords.append(movieCategory)
        }

        let stars = movie["Stars"]!.componentsSeparatedByString(", ")
        for star in stars {
            keywords.append(star)
        }

        searchableItemAttributeSet.keywords = keywords
    }
}

请注意在MoviesData.plist文件中电影的类型是一个用逗号进行分隔的单一字符串。所以我们要将里面所有的种类都分离出来后存储到数组变量movieCategories中。然后再使用循环将里面的每个类型添加到关键字数组keywords中。对于演员明星,我们使用一样的步骤进行处理。

上面代码中最重要的是最后一步;我们将关键字数组设置到每个电影的属性中。忘记这一行代码的话,Spotlight中将不会显示任何结果。

现在我们已经有了关键字属性了,下面该实例化可搜索对象了并将该对象添加到可搜索对象数组中。

func setupSearchableContent(){
    var searchableItems = [CSSearchableItem]()

    for i in 0...(moviesInfo.count - 1) {
        ...

        let searchableItem = CSSearchableItem(uniqueIdentifier: "com.appcoda.SpotIt.\(i)", domainIdentifier: "movies", attributeSet: searchableItemAttributeSet)

        searchableItems.append(searchableItem)
    }
}

上面的实例化接受三个参数:

  • uniqueIdentifier: 这是当前可搜索对象在Spotlight中的唯一标识。你可以以你自己喜欢的方式编写这个标识,但是又一个细节需要注意:在这个例子里,我们将当前索引添加到标识里,因为后面我们需要这个索引来查找匹配要显示的电影细节。一般情况下,将指向要显示数据细节的值添加到标识里是一个不错的想法。真能让你更好的理解电影的索引值的意义。

  • domainIdentifier: 使用这个参数将可搜索对象组合到一起

  • attributeSet: 这是我们刚才进行复杂设置的属性。

最后,这个一个新的可搜索对象被添加到searchableItems数组中了。

还有最后一步我们需要做:就是使用Core SpotlightAPI对可搜索对象简历索引。该步骤是在for循环外面完成的。

func setupSearchableContent() {
    ...

    CSSearchableIndex.defaultSearchableIndex().indexSearchableItems(searchableItems) { (error) -> Void in
        if error != nil {
            print(error?.localizedDescription)
        }
    }
}

上面函数完成后,我们需要在viewDidLoad()函数对它进行调用:

ovrride func viewDidLoad() {
    ...

    setupSearchableContent()
}

显示演示应用已经能够使用Spotlight搜索到结果了。运行代码并退出应用到Spotlight中使用关键词进行搜索吧。与之相关的数据会展现在你眼前。点击这些结果,应用会自动启动。

clipboard.png

更具针对性的目标

让应用数据能够在Spotlight被搜索到已经很不错了,然而我们还可以做的更好。当我们点击结果的时候,应用会启动并切换界面到电影列表界面,但是我们的目标当点击的时候直接跳转到详细介绍界面。

虽然这样做听起来很困难和复杂,但是最终你会看到其实很简单。在这个演示应用中这个就更简单了,我们基于已有的东西以便管理那些点击选中后需要展示的电影的详细信息。

在这里我们的主要工作的重载UIKit中名为restoreUserActivityState的函数,并且处理Spotlight中的点击事件。我们最终要实现的是根据Spotlight中的结果的标识找到其在moviesInfo数组中对于的索引号(如果你还记得的话该标识是在前面一部分创建的),然后依据改索引号将正确的数据传递到MovieDetailsViewController中去。

上面那个函数的参数是一个NSUserActivity对象。该对象又一个名为userInfo的字典属性,该属性中含有在Spotlight中所选结果的标识。从这个标识里面我们就能获得选中电影在moviesInfo数组中的索引,然后我们将对象的数据传递过去。这就是函数的整个过程。

下面是具体的实现:

override func restoreUserActivityState(activity: NSUserActivity) {
    if activity.activityType == CSSearchableItemActionType {
        if let userInfo = activity.userInfo {
            let selectedMovie = userInfo[CSSearchableItemActivityIdentifier] as! String
            selectedMovieIndex = Int(selectedMovie.componentsSeparatedByString(".").last!)
            performSegueWithIdentifier("idSegueShowMovieDetails", sender: self)
        }
    }
}

正如你所看见,首先需要检查activity的类型是不是CSSearchableItemActionType类型。老实说,在这个程序里这么做其实不是很重要,但是如果你需要处理应用中多个NSUserActivity对象的话你就不能忘记需要这样做(例如:iOS8中就引入的Handoff特性中使用的NSUserActivity类) 。在userInfo字典里的标识符是一个字符串。当你获得标示后,我们按照.号来分解该字符串,其中最后一个久代表着我们选中电影在集合中的索引。剩余的代码就很容易了:我们将这个索引赋值给selectedMovieIndex变量,然后再触发界面切换。之前的代码已经实现了切换。

现在我们打开AppDelegate.swift文件。我们需要实现一个代理函数。该函数在每次Spotlight中与演示应用相关的数据被选中点击时都会被调用,而的职责就是调用该函数并传递用户激活的对象。下面就是代码实现:

func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]?) -> Void) -> Bool {
    let viewController = (window?.rootViewController as! UINavigationController).viewControllers[0] as! ViewController
    viewController.restoreUserActivityState(userActivity)

    return true
}

在上面代码片段中,我们访问windowview controller的属性并恢复用户状态。当然,我们也可以采用下面的方法实现:使用NSNotificationCenter并传递一个自定义notification,然后在ViewController里处理该通知。但是前面的方法更直接一点。

好了,教程到此为止。演示应用应该能完成所有想要的结果了。

clipboard.png

总结

对于开发者来说iOS9中的新的搜索API看起来一片美好,因为这让我们的应用更容易被用户发现的使用。在这篇教里面,我们所有的工作都是围绕着让应用中的数据能够在Spotlight搜索中能被搜索到,以及当用户选中搜索结果时该如何处理事件将详细的内容展示出来。在你自己的应用中实现该功能的话绝对会提升用户体验,因此该特性时你需要认真思考添加到你当前或者以后的工程中的。再一次我们来到了文章的结尾,我希望这个教程对你有帮助。

注意:原文中的其实工程被墙了。我为大家献上微云链接。完整的工程作者并没有提供,下面是我根据教程完成的Spotlt


BigNerdCoding
1.2k 声望125 粉丝

个人寄语: