本文由CocoaChina译者KingOfOnePiece(博客)翻译
作者:GABRIEL THEODOROPOULOS?校对:hyhSuper
iOS每一次版本的更新,都会给全球的开发工作者带来新的“知识点”和对现有技术进行的改进。显然,iOS的最新版本iOS 9不仅延续了这一传统,还公布了新的框架和API,开发者可使用新增的框架和API让自己的应用表现的更出色。其中之一就是Core Spotlight框架,它包含了一些优秀的API,有待开发者深入探究。
Core Spotlight (CS) 框架是Search API的一部分,它增加了开发者的应用的曝光率,使用户更容易发现和访问APP,但是这些API在iOS9之前的版本中是不可用的。这些Search API拉近了用户和app的距离,用户可以通过这种新的方法迅速的找到app,而app也可以迅速的对用户的操作做出反应。除了Core Spotlight之外,iOS 9还包含以下新的search功能(仅供参考):
1、NSUserActivity这个类中新的方法和属性(该类负责存储app关闭时的状态,以便之后恢复app的状态。)
2、在设备上使用web标记,以便搜索到web中的内容。
3、能通过web内容中的链接直接启动应用程序的通用链接。
本文不会对以上三点做详细介绍,只详细讲解Core Spotlight框架。在讲解之前,先了解以下Core Spotlight到底是什么?
如上图所示:Core Spotling框架可以让用户通过Spotlight搜索到app中的数据,并将app相关内容和系统搜索返回的结果一起展示出来。也就是说用户可以与应用程序的其他相关结果进行交互,当点击其中一项搜索结果时并不仅仅自动启动APP,开发者也可以让用户选择与数据最相关的内容。
从开发者的角度来说,集成Core Spotlight框架并使用它提供的API并不是一件难事。学过这教程之后,你将会发现仅仅需要几行代码就可以实现这个功能。集成的核心问题在于开发者必须把APP的数据以特定的格式加入到iOS系统的索引中。
由于本教程专注于Core Spotlight框架的使用,所以我不打算深入研究这个框架。
关于Demo App
和往常一样,我们打算通过一个示例应用来研究探索Spotlight的细节。在这个demo中,我们将往应用程序中写入一组数据,并且这些数据都是可以通过真机或模拟器的Spotlight功能被搜索到的。虽然实现Spotlight功能搜索是重点,但是还是有必要介绍一下关于demo app 的更多细节。
我们的示例应用要达到的效果是展示一些电影及其相关的信息,例如电影概要、导演、明星以及影评等级等。所有的电影数据将会放在列表中展示,并且当点击到对应的行时,所选择的电影会在一个新的视图控制器中展示出电影的详情。这些功能和数据作为我们挖掘Spotlight的API而言已经够用了。我们的数据来源是International Movie Database (IMDB),我们从这个网站获得数据示例。
通过看下面的动画示例,你可以先看一下演示程序的效果:
在本教程中,我们有两个目的:最重要的是让这个应用内的所有电影方面的数据都可以被Spotlight搜索到。当用户通过关键字搜索时,应用程序内关于电影的数据将会展现给用户。设置这些关键字是我们接下来工作的一部分,我们必须重新定义它们,使之符合规则。
点击搜索结果会打开应用程序,之后我们要实现第二个目标。如果我们没有做任何的处理,那么将会加载包含有电影数据列表的默认视图控制器展示给用户。然而,假如我们考虑到用户体验的话,这么处理并不是很好。好的方案是,我们的APP应该展示Spotlight选中的电影的详情。简言之,我们不仅要让应用程序内的电影数据能够通过Spotlight被搜索到,而且还要在用户点击搜索结果时,展示关联的电影详情页。接下来的示例动画会讲解的更清楚:
?
为了节约时间,你可以在这里下载工程,然后开始我们的工作。在工程里面你会发现如下所示的内容:
界面部分的组件已经搭建完成,并且包含所有必要的IBOutlet属性。
最小化的列表视图。
所有的电影数据都存放在以plist为后缀的文件中。此外图片文件是和电影一一对应的(一共有5部)。
假使你想知道预先准备的文件中包含了哪些对应电影的数据,那么你可以通过这个截图示例看到所有包含的内容。
先看一下Core Spotlight API的具体信息,我们会处理下面两个任务:
1、在列表中加载和展示电影数据。
2、在选中电影数据列表的某一行时会跳转到对应的电影详情界面。
启动项目没有实现上边的内容,尽管那样么做会让你们快速进到我们的话题,原因很简单:我很确信通过演示应用程序的核心功能和数据样本,你会很容易地了解到将要被Spotlight搜索到的具体数据。但是别担心,因为所有准备工作花费的时间很少,并且很快就可完成。
加载和展示示例数据
假如此时你已经下载好了起始工程,并且已经看过了电影数据属性列表,那么就让我们开始Coding吧。在MoviesData.plist这个文件夹下,你可以看到一共有5条数据,这些数据是从IMDB网站上随机选取的他们对应5个电影示例。我们第一步是从plist文件中加载数据到一个数组中,然后在列表中展示他们。
废话少说,直接上代码。打开ViewController.swift这个文件,并且在这个类声明属性的地方这么写:
1 | var ?moviesInfo:?NSMutableArray! |
所有的电影模型数据都会被加载到moviesInfo可变数组中,单个电影模型的数据将会以键值对的方式保存在字典中,并且他们和文件中的属性列表相匹配。
现在让我们编写一个加载数据的自定义函数。接下来你会看到,我们只是确保属性列表文件是否存在,如果存在,我们就初始化数组的文件内容:
1 2 3 4 5 | func loadMoviesInfo() {
if let path = NSBundle.mainBundle().pathForResource( "MoviesData" , ofType: "plist" ) { moviesInfo = NSMutableArray(contentsOfFile: path) } } |
接下来我们需要在viewDidLoad()函数调用loadMoviesInfo()函数。只是为了确保你在调用configureTableView()函数前正确调用此函数,下面展示的是代码片段:
1 2 3 4 5 6 7 8 9 | override func viewDidLoad() {
super .viewDidLoad()
//Load the movies data from the file. loadMoviesInfo() configureTableView() navigationItem.title?= "Movies" } |
要注意的是,我们只需把文件内容加载到viewDidLoad()函数,而不是为了创建上述的loadMoviesInfo()函数,但是作为喜欢代码整洁的人,即便是这么小的事情,都会选择更好的方式来实现。
应用程序每次启动时都会加载这些电影数据,我们可以继续修改当前视图列表,让它显示电影信息。需要处理的只有这么多:根据电影数据信息定义列表的行数,然后在表视图的单元格中展示正确的电影数据。
从列表视图的行数开始,显然列表视图的行数等于电影的数目。然而,我们不应忘记,要确保列表视图中有电影数据展示出来,否则当文件内容不被加载到数组中时就会造成应用程序的崩溃。
1 2 3 4 5 6 | func?tableView(tableView:?UITableView,?numberOfRowsInSection?section:?Int)?->?Int?{ if ?moviesInfo?!=?nil?{ return ?moviesInfo.count }
return ?0 } |
最后,让我们展示电影数据。为了达到我们需要的目标,在开始准备的工程中你可以找到UITableViewCell的子类MovieSummaryCell,和代表一个电影单元格的.xib文件:
用这样的一个单元格来展示每个电影的图片、标题、部分的描述信息和电影评级。所有UI控件已经连接到IBOutlet属性,并且可以在MovieSummaryCell.swift这个文件找到对应属性的名称:
1 2 3 4 | @IBOutlet?weak? var ?imgMovieImage:?UIImageView! @IBOutlet?weak? var ?lblTitle:?UILabel! @IBOutlet?weak? var ?lblDescription:?UILabel! @IBOutlet?weak? var ?lblRating:?UILabel! |
上面的名字代表各个属性的用途,现在我们已经能够看到它们,我们通过它们来展示电影的相关细节。回到ViewController.swift文件,根据下面的代码片段更新表视图的函数:
1 2 3 4 5 6 7 8 9 10 11 | func?tableView(tableView:?UITableView,?cellForRowAtIndexPath?indexPath:?NSIndexPath)-> let?cell?=?tableView.dequeueReusableCellWithIdentifier( "idCellMovieSummary" , 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这个字典不是必需的,但是它让上面的简单代码编写的更加容易。
这时你可以运行一次你的应用程序,并且可以看到电影的细节被展示在表视图。到目前为止,我们所做的这些是大家所熟知的,所以直接到第二个步骤:显示所选电影的细节。
数据细节展示
在MovieDetailsViewController这个类中,我们将要展示在ViewController类中tableView上选中的电影的细节。所有的界面构建已经完成,现在我们必须做两件事:把包含电影信息的字典从ViewController类传到MovieDetailsViewController类中,然后从字典取值赋值给这个类中相应的UI控件,这些控件的IBOutlet属性都已经被声明并且已经正确的连接到单个UI组件上。
所以,说到字典,让我们在MovieDetailsViewController类中做下面的声明:
1 | var ?movieInfo:?[String:?String]! |
回到ViewController.swift文件,让我们看一下点击表视图的行时需要做的工作有哪些。当点击列表的一行时,我们需要知道点击行的索引,以便于从moviesInfo数组选择合适的字典,然后在视图跳转的时候把字典传递到下一个视图控制器。从tableview的委托方法中获取行索引是很容易的,但是我们需要一个自定义的属性来存储它,因此在ViewController类的顶部我们需要这么声明:
1 | var ?selectedMovieIndex:?Int! |
然后,我们需要用下面的方式处理tableView中选中row的索引:
1 2 3 4 | func?tableView(tableView:?UITableView,?didSelectRowAtIndexPath?indexPath:?NSIndexPath)?{ selectedMovieIndex?=?indexPath.row performSegueWithIdentifier( "idSegueShowMovieDetails" ,?sender:?self) } |
有两个简单的事情需要处理:第一是把点击的行的索引存储在我们自定义的selectedMovieIndex属性中,然后执行跳转到展示电影详情的界面。然而,这是不够的,因为我们还未从moviesInfo数组中选择相应的包含电影信息的字典,并把它传递到MovieDetailsViewController这个类中。我们该怎么做呢?重写prepareForSegue:sender:函数,并实现我刚刚描述的功能。下面是代码,请看:
1 2 3 4 5 6 7 8 | override?func?prepareForSegue(segue:?UIStoryboardSegue,?sender:?AnyObject?)?{ if ?let?identifier?=?segue.identifier?{
if ?identifier?==? "idSegueShowMovieDetails" ?{ let?movieDetailsViewController?=?segue.destinationViewController?as! movieDetailsViewController.movieInfo?=?moviesInfo[selectedMovieIndex]?as!?[String?:?String] } } } |
很简单,我们只通过destinationViewController的segue属性获得MovieDetailsViewController实例,然后我们将包含电影信息的字典值赋给我们这部分开始时声明的movieInfo属性。
现在,再次打开MovieDetailsViewController.swift文件,我们将会在这里定义一个自定义的函数。在它里面,我们将会从movieInfo字典中取值赋给那些相应的控件,那么这部分的工作到这里就就结束了。下面的代码是一个简单的实现,所以不再做进一步的探讨:
1 2 3 4 5 6 7 8 9 | 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:中函数调用上面的函数,代码如下
1 2 3 4 5 6 | override?func?viewWillAppear(animated:?Bool)?{
if ?movieInfo?!=?nil?{ populateMovieInfo() } } |
这一部分已经结束。再运行一下你的应用程序,一旦你选中tableView中的某一个电影机就会看到电影的详细介绍。
为Spotlight添加app的索引数据
通过使用iOS9中提供的Core Spotlight 框架,使得任何一款应用都可以通过Spotlight功能被搜索到。在Spotlight上通过用户的搜索行为找到app的关键在于使用Core Spotlight API索引到我们应用的数据。但是既不是我们的app也是不是CS API接口决定应该设置什么类型的数据。所以我们需要提供特定的数据格式给 API接口。
再具体点来说,就是我们想通过Spotlight功能搜索到得数据必须封装成CSSearchableItem类型的对象,然后一起组装到数组中并把这个数组传递给CS API 中供它索引。单个的CSSearchableItem对象包含一系列的属性,这些属性使得iOS系统可搜索对象的细节更加清楚。像在搜索时应该展示哪些数据片段(例如:电影名称、图片和影视简介等),哪些app的数据关键字需要在Spotlight上展示。CSSearchableItemAttributeSet 类对象为 CSSearchableItem对象提了许多属性,只需为我们所需要的属性赋值就好了。查看官方文档,你可以找到所有支持的属性。
最后一步通常要做的是为Spotlight功能添加索引数据。一般情况下,实现流程如下步骤所示(包含添加索引):
1、给每一条数据设置属性,例如电影模型数据(设置CSSearchableItemAttributeSet对象)。
2、使用上一步的属性值(CSSearchableItem对象),为每一条数据初始化搜索项并赋值。
3、收集所有的可搜索项放到一个数组中。
4、使用上个步骤的数组作为Spotlight的索引数据。
我们将按照上面的步骤一个一个的实现,为了达到目标,我们将会在ViewController.swift这个文件内创建一个叫做setupSearchableContent()的函数。在这部分功能实现结束之际,你会发现实现让你的数据可以被搜索到的功能一点也不难。不过,我不会一次性给你所有的实现代码;相反,为了你更容易理解,我打算把代码分为片段。别担心,代码量并不是那么多。
在我们开始实现新功能之前,我们必须先导入两个框架:
1 2 | import?CoreSpotlight import?MobileCoreServices |
我们定义一个新的方法,同时在方法中声明一个用来存放我们搜集到的搜索项的数组:
1 2 3 | func?setupSearchableContent()?{ var ?searchableItems?=?[CSSearchableItem]() } |
现在在一个for循环中访问每一个包含电影信息的字典:
1 2 3 4 5 6 7 | func?setupSearchableContent()?{ var ?searchableItems?=?[CSSearchableItem]() for ?i? in ?0...(moviesInfo.count?-?1)?{ let?movie?=?moviesInfo[i]?as!?[String:?String] } } |
我们将为每一部电影创建一个CSSearchableItemAttributeSet对象,然后我们将为搜索项数据设置属性,这些搜索项数据将会在用户使用Spotlight搜索时作为搜索结果展示出来。在demo App中,我们将把电影的标题、简介和图片作为数据展示给用户。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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],
//?Set?the?description. searchableItemAttributeSet.contentDescription?=?movie[ "Description" ]! } } |
注意上面的片段代码中我们是如何设置电影数据模型中图片属性的。有两种实现方法:一种是为imge制定一个URL链接,另一种是为image提供一个NSData对象。最简单的方法是为每一个电影图片文件提供一个URL,因为我们知道它们就在程序应用包里面。然而,这样做就需要我们把每个图片文件名分割成实际名称和扩展类型,因此我们用String类的 componentsSeparatedByString:方法将这两个值分开。剩下的代码就容易理解了。
现在是时候为app在Spotlight上设置我们想要的data的关键字了。指定关键字之前要认真考虑,因为你的决定对用户和App在Spotlight上的曝光率起着最终的决定性作用。在demo App中我们把电影所属的类别和演员设置成关键字。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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数组中以方便及时访问。然后用一个for循环在keywords数组中添加分类的关键字。对电影数据模型的明星属性使用相同的步骤,也就是说我们再次把包好所有明星名字的字符串分割成单独的值,然后把他们存入keywords数组中。
上面的片段代码中最后一句代码是很重要的,我们为每一个电影数据模型设置了关键字属性。忘记了这一点,当用Spotlight搜索时将不会出现任何关于我们app的结果。
我们已经为Spotlight设置了属性和关键字,那么现在是时候初始化搜索项,并且把它们添加到searchableItems数组中,代码如下:
1 2 3 4 5 6 7 8 9 10 11 | 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) } } |
上述searchableItem对象的初始化接受了三个参数:
uniqueIdentifier:这个参数唯一地标识Spotlight当前搜索项。你可以用你喜欢的方式构造这个唯一标示符,但是注意一个细节:在这个例子中,我们为当前电影添加了索引标示符,因为之后我们需要用它来匹配索引相对应得电影细节展示界面。一般而言,在唯一标识符的值里面添加详情页要展示的数据是个好主意。一会你将会更好的理解电影数据模型的索引值的用途。
domainIdentifier:使用这个参数对搜索项进行分组。
attributeSet:它就是我们刚刚设置属性时的属性设置对象。
在最后,把新的搜索项添加到searchableItems数组中。
还有最后一步需要做,用Core Spotlight的API去索引这些搜索项。它发生在for循环之外地方:
1 2 3 4 5 6 7 8 9 | func?setupSearchableContent()?{ CSSearchableIndex.defaultSearchableIndex().indexSearchableItems(searchableItems) in if ?error?!=?nil?{ print(error?.localizedDescription) } } } |
上面的函数已经完成,但是我们需要调用它。我们将会在viewDidLoad()函数中调用它:
1 2 3 4 5 | override?func?viewDidLoad()?{ setupSearchableContent() } |
现在我们准备第一次开始用Spotlight搜索电影。运行app,然后退出app使用Spotlight搜索上面我们设置的关键字。结果将会展现到你眼前,通过点击搜索结果中的任意项,这个app将自动启动。
实现目标
当用Spotlight搜索时,能够从应用程序中搜索到电影数据是会给人深刻的印象,但是我们还可以比这做的更好。现在,当选择一个搜索结果时,就会启动应用程序,并且展示出ViewController这个界面,但是我们目标是直接进到展示电影细节界面并可以直接看到所选择电影的信息。
尽管这听起来比较困难,或者难以实现,但是最终你会发现它很简单。在这个指定的demo app中,根据我们现有的数据展示所选电影项的详细信息会更简单。
这里主要的工作是重写UIKit中的一个名字叫做restoreUserActivityState:的函数,并处理Spotlight上选中的结果项。我们最终的目标是从搜索项的标识符(如果你还记得话,我们在上一个部分动态的创建了标识符)中提取出在moviesInfoarray数组中的电影模型的索引值,然后用它传递一个正确的电影数据字典并展示MovieDetailsViewController视图。
restoreUserActivityState:函数的参数是NSUserActivity对象。这个对象有一个字典类型的属性名为userInfo,这个字典包含Spotlight上选中搜索项的标识符。从这个标识符中我们可以提取出moviesInfo数组中电影数据模型的索引值,然后我们就可以展示电影详情页面的视图了。就这么多,下面看一下代码实现:
1 2 3 4 5 6 7 8 9 | 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 type是否和CSSearchableItemActionType匹配。老实说,在示例程序中这个并不重要,但是如果你在你的应用程序中处理多个NSUserActivity对象的情况下,有些事是不应该忘记的做的(例如,Handoff feature是ios8首次提出来的,并且它充分使用了NSUserActivityclass类)。在useInfo字典中identifer对象是一个字符串。一旦我们得到它,我们将基于点符号截取这个字符串,我们从截取的字符串数组中取出最后一个对象就是选中的电影数据模型在电影数据模型数组中的索引值。剩下的就工作就比较简单:将这个索引值复制给selectedMovieIndex属性,然后执行跳转。我们之前的实现将会完成剩余的工作。
现在切换到AppDelegate.swift文件。我们需要实现一个现在还未实现的代理函数。每次点击在Spotlight上搜索的一个结果时,那个函数就会调用一次,我们现在的任务是调用我们上面已经实现的函数,传递userActivity对象,请看下面的实现代码:
1 2 3 4 5 | func?application(application:?UIApplication,?continueUserActivity?userActivity: let?viewController?=?(window?.rootViewController?as!?UINavigationController).viewControllers[0]?as! viewController.restoreUserActivityState(userActivity) return ? true } |
在上面的代码片段中,我们要做的第一件事就是通过window属性访问ViewController视图控制器,以恢复到之前的用户活动状态。或者,不用上面的方法,你可以用NSNotificationCenter类发送一个自定义的通知,然后在ViewController 类中处理这个通知,但上面的方法更直接。
就这么多!我们的示例应用已经完成了,所以再次运行它,看看你在使用Spotlight搜索电影时会发生什么。
总结
iOS 9 中新的搜索API对开发者来说看起来相当的吸引人,它们使得用户更容易发现和访问APP。在这个教程中我们实现了所有Spotlight搜索App数据的相关操作,包括方便用户在Spotlight上搜索时App数据索引,以及如何把选中的结果项通过app处理后的数据展示给用户。在你的应用程序实现这样的功能肯定会提升用户体验,所以你应该在你当前和未来的项目认真考虑使用它。又到了结束的时候,我真希望你能发现这篇文章的有用之处!
作为参考,你从GitHub上可以下载完整的工程。