前言:
先放出原文地址: AppCoda, 支持原创作品哈.
本文为上面这篇文章的译文, 原文中使用Swift
语言, 本文使用Objective-C
语言, 其中大部分内容为翻译(其中有些没用的废话就没翻译), 有一部分自己的理解. 示例代码也有修改, 和原文中有出入. 喜欢看英文文档的小伙伴请直接查看原文.
伴随着每一个新版本 iOS 的更新, 苹果都会给全世界的开发者们带来一些新的技术, 当然 iOS9 也不会违背这个传统. 其中之一就是 Core Spotlight
框架, 它包含了许多很棒的 APIs, 一旦开发者们将它们集成在自己的应用中, 就可以让他们的应用提升到一个新的高度.
Core Spotlight
框架是苹果提供的 APIs
集合中的一部分, 被称之为 Search APIs
. 它可以帮助你很有效的拉近与用户之间的距离, 让用户更容易访问你的应用程序. 除了 Core Spotlight
之外, iOS9 中其它的搜索功能还包括:
-
NSUserActivity
: 新的方法和属性(用来保存 App 当前状态, 并在以后恢复该状态). -
Web Markup
: 可以让你在设备中搜索 Web 中的内容. -
Universal Links
: 通过 Web 直接开启应用程序.
在本篇文章中, 我们不会去讨论上面这三个搜索功能, 我们只专注于 Core Spotlight
框架. 首先我们来看一下 Core Spotlight
到底是什么.
Core Spotlight Framework 可以让你 App 中的内容在 Spotlight
中搜索到, 并且将相关的搜索结果展现给用户, 并且允许用户和搜索的结果进行交互. 当用户选择了其中一个搜索的结果后, 不但可以自动的打开你的应用程序, 同时还可以跳转到指定的页面来查看详细的内容.
从开发者的角度来看, 集成 Core Spotlight
框架并使用它的 APIs
并不复杂. 通过接下来的教学, 你会发现必要的代码可能也就只有几行就够了. 其中最核心的处理过程就是开发者需要让 iOS
对应用程序中的数据进行索引操作.
本篇教学主要是针对 Core Spotlight
框架, 但是我并不打算在本篇文章中对一些细节部分进行讲解. 如果你有幸去学习一些我个人认为很棒的东西, 那就请继续阅读本篇文章. 我很自信的告诉你, 在你阅读完本篇文章之后, 你会发现集成 Core Spotlight
框架, 以及使用 Spotlight
搜索你应用中的内容是多么的简单.
关于 Demo
和往常一样, 我们会通过一个实例程序来进行讲解和学习. 这一次我们的重点内容是在 App 内填充一些数据, 并允许这些数据在 Spotlight
中进行搜索. 除了重点之外, 我们还需要更多的了解一下实例程序.
实例程序的主要目的是为了展示一些电影相关的数据, 例如: 简介、导演、评分等. 所有的数据都会用 TableView
来进行展示, 当选中一个电影, 会进入到一个新的详情页面来进行展示. 基本上就是这些内容, 我们通过这些数据以及功能来学习 Core Spotlight
是如何工作的. 实例程序中的数据来自这里.
通过下面这张动图, 来感受一下实例程序.
在本篇教学中, 我们有两个目标: 最重要的一个目标是, 我们需要让示例程序中的所有电影数据都可以在 Spotlight
中通过关键词来进行搜索. 当然, 设置关键词也是我们的任务之一.
当用户点击了一条搜索结果, 应用程序将会自动打开, 接下来就是我们的第二个目标, 如果我们不做任何处理, 那么默认的视图控制器将会被加载并呈现在用户眼前, 也就是我们的首页(电影列表的那个页面). 然而当我们站在用户体验的角度来思考这个问题, 其实这样并不是一个完美的解决方案. 完美的解决方案是, 当用户选择了一个搜索结果, 我们应该启动应用程序, 并给用户展示对应电影的详情数据, 这就是我们得第二个目标. 简单来说, 我们不仅要做到在 Spotlight
中所搜到应用中的电影, 我们还要在用户选择了某一个电影后, 开启应用程序并进入到详情页面来显示电影的详情信息. 看下面这张动图, 你就明白了:
为了不浪费时间, 你可以在这里下载实例程序. 在实例程序中, 你将会看到以下的内容:
-
UI
以及IBOutlet
已经完成. - 最简单的实现了
UITableView
. - 所有的电影数据都在
.plist
文件中, 另外还包含了对应的图片(一共5个).
用一张图片来解释, 下图中展示了 .plist
文件中包含数据以及数据的结构.
在学习 Core Spotlight
之前, 先来完成两个小任务:
- 加载数据, 并填充在
UITableView
中. - 在
Detail View Controller
中显示选中的电影详情.
在实例工程的初始阶段, 如果我实现了上面这两个功能会使我们更快的进入 Core Spotlight
的学习阶段, 但是我并没有这么做, 原因很简单: 我相信通过这个过程, 你会更容易理解这些数据是如何从一些普通的数据变为 Spotlight
可搜索到的数据. 不用担心, 实现上面两个功能很简单, 我们会很快完成的.
加载、展示实例数据
OK, 我们现在就可以开始了, 假设你现在已经下载好了实例程序, 并且已经对 .plist
文件中的电影数据有了一定的了解. 我们的第一个任务是将 .plist
文件中的数据加载到数组中, 并将它们填充到 TableView
中.
我们直接开始写代码, 打开 MovieListViewController.m
文件, 先来定义一个数据源数组:
@property (nonatomic, strong) NSMutableArray *moviesInfo;
所有的电影数据, 都会被加载到这个数组中. 每一个电影数据都用一个 MovieModel
来表示. 这个数据模型的逻辑已经写完了, 有兴趣的同学可以看看, 本人实在是懒得导入 Mantle
或 YYModel
框架了, 所以就直接用 objectForKey
解析数据了. 哈哈! 尴尬!
.plist
数据转数据模型的代码已经封装在 MovieModel
中了, 在 MovieListViewController.m
中添加以下代码:
#pragma mark - Lazy Load
- (NSMutableArray *)moviesInfo {
if (!_moviesInfo) {
_moviesInfo = [MovieModel models];
}
return _moviesInfo;
}
接下来我们来修改 TableView
的数据源方法, 来展示数据: 首先根据数据源的数量, 返回 Row
的个数, 然后在 Cell
中展示相应的内容.
我们从 numberOfRows
方法开始修改, 很明显在这里我们应该返回数据源数组的个数.
#pragma mark - UITableView DataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.moviesInfo.count;
}
最后, 我们来将数据显示在 TableView
中, 在工程中你会看到一个 UITableViewCell
的子类 MovieCell
, 它包含了一个 Xib
文件用来描述 Cell
的样式.
MovieCell
展示了电影的图片、标题、一部分的描述和电影评分. 所有的 UI
控件都已经生成了对应的 IBOutlet
属性, 你可以在 MovieCell.h
中进行查看:
@property (nonatomic, weak) IBOutlet UIImageView *movieImageView;
@property (nonatomic, weak) IBOutlet UILabel *labelTitle;
@property (nonatomic, weak) IBOutlet UILabel *labelDesc;
@property (nonatomic, weak) IBOutlet UILabel *labelRating;
上面这些属性的名字已经很明显的代表了它们所对应的 UI
控件, 现在我们就用它们来展示电影的内容. 回到 MovieListViewController.m
文件中, 利用下面的代码块更新 cellForRowAtIndexPath
方法:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MovieCell *cell = [tableView dequeueReusableCellWithIdentifier: @"MovieCell" forIndexPath: indexPath];
MovieModel *movieModel = self.moviesInfo[indexPath.row];
cell.labelTitle.text = movieModel.title;
cell.labelDesc.text = movieModel.desc;
cell.labelRating.text = movieModel.rating;
cell.movieImageView.image = movieModel.image;
return cell;
}
OK, 现在你可以将实例程序跑起来看一下效果了, 至今为止我们所做的这些, 对于每一个开发者来说都应该是非常简单的, 所以在这里不做过多的讲解, 直接进入下一个阶段: 选中一部电影, 在详情页面展示电影的详细内容.
显示电影详情
在 MovieDetailViewController.m
文件中, 我们将会显示选中电影的详情信息. 在 Storyboard
中我已经添加好了详情视图控制器. 在这里我们需要做两件事: 首先是将 MovieListViewController
中选中电影的数据模型传递给 MovieDetailViewController
. 其次是利用数据模型的内容对 MovieDetailViewController
的 UI
控件进行数据的填充.
将 MovieDetailViewController.h
文件该为下面代码块显示的一样:
@class MovieModel;
@interface MovieDetailViewController : UIViewController
@property (nonatomic, strong) MovieModel *movieModel;
@end
接下来我们暂时先回到 MovieListViewController.m
文件中, 来看一下当选中了一部电影之后, 我们需要做哪些事情. 当点击事件发生之后, 我们希望知道点击的是第几部电影, 并且获取到数据源数组中对应的数据模型, 并将它传递给 MovieDetailViewController
. 利用 TableView
的代理方法来获取对应的数据模型很简单, 但是我们需要将它保存下来, 所以在 MovieListViewController.m
中我们需要再定义一个成员变量:
@interface MovieListViewController () {
NSInteger _selectedMovieIndex;
}
然后我们来处理 didSelectRowAtIndexPath
方法, 在 MovieListViewController.m
文件中添加以下代码:
#pragma mark - UITableView Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath: indexPath animated: YES];
_selectedMovieIndex = indexPath.row;
[self performSegueWithIdentifier: @"idSegueShowMovieDetails"
sender: self];
}
我们在该方法中做了三件事: 第一件事是取消了 Cell
的选中状态, 第二件事是将选中的行数保存了下来, 第三件事是执行了一个 Segue
(Push
出 MovieDetailViewController
). 然而仅做这三件事还是不够的, 因为我们还没有从数据源数组中获取到对应的数据模型, 并且我们还没有向 MovieDetailViewController
传递任何数据. 那我们现在该怎么做呢? 很简单, 只需要复写 prepareForSegue:sender:
方法, 来看下面的代码块:
#pragma mark - Override Methods
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString: @"idSegueShowMovieDetails"]) {
MovieDetailViewController *detailViewController = segue.destinationViewController;
detailViewController.movieModel = self.moviesInfo[_selectedMovieIndex];
}
}
非常简单, 我们通过 segue
的 destinationViewController
属性来获取 MovieDetailViewController
的实例对象, 然后我们从 moviesInfo
数据源数组中获取到了相应的数据模型, 并将数据模型传递给了 MovieDetailViewController
.
现在, 回到 MovieDetailViewController.m
中, 我们添加下面这段代码:
#pragma mark - Private Methods
- (void) populateMovieInfo {
self.titleLabel.text = self.movieModel.title;
self.categoryLabel.text = self.movieModel.category;
self.descLabel.text = self.movieModel.desc;
self.directorLabel.text = self.movieModel.director;
self.starsLabel.text = self.movieModel.stars;
self.ratingLabel.text = self.movieModel.rating;
self.movieImageView.image = self.movieModel.image;
}
我们将数据模型中的内容填充到了控件中, 注意, 我们需要在 viewDidLoad
中调用该方法.
#pragma mark - Life Cycles
- (void)viewDidLoad {
[super viewDidLoad];
[self setupUI];
[self populateMovieInfo];
}
这一部分的内容基本就这么多了, 现在你可以跑一下示例程序来看看效果. 然后我们就开始进入下一阶段的学习.
对数据进行索引操作
使用 Core Spotlight
框架, 可以让应用中的内容在 Spotlight
中搜索到, 达成这一目的的关键步骤就是调用 Core Spotlight
的 API
来对相应的数据进行索引操作, 这样一来, 当用户使用 Spotlight
进行搜索时, 就能搜索到相应的数据了. 至于哪些数据可以被搜索到, 不是应用程序决定的, 也不是 Core Spotlight
决定的, 而是我们决定的, 所以我们有责任为 Core Spotlight
提供数据, 告知哪些数据是允许被搜索的.
所有允许被搜索到的数据, 都被描述成为一个 CSSearchableItem
对象, 将这些对象放到一个数组中, 递交给 Core Spotlight
框架中相应的 API
来进行索引操作. 一个 CSSearchableItem
中包含了一系列的属性用来描述一个允许被搜索的数据, 例如: 电影名称、图片、描述、搜索关键词等... 在 CSSearchableItem
中所有的这些属性都被描述成为了一个 CSSearchableItemAttributeSet
对象, 该对象中提供了这些我们可能需要用到的属性. 作为参考, 在这里给你一个官方文档的链接进行查看.
对数据进行索引操作是最后一步, 也是必须要做的一步. 一般包含以下几个步骤:
-
CSSearchableItemAttributeSet
: 设置搜索对象的属性. -
CSSearchableItem
: 为每一个搜索数据创建一个CSSearchableItem
对象, 并关联第一步中生成的属性对象. - 将所有的
CSSearchableItem
对象放入到数组中. - 将数组递交给
Core Spotlight
对应的API
对数据进行索引操作.
我们接下来会一步一步的根据上面这四个步骤进行操作. 不过在这之前我们需要在 MovieListViewController.m
中新增一个方法叫 - (void)setupSearchableContent
. 当我们完成这个方法的实现部分之后, 你会发现其实很简单. 不过我不会把所有的实现代码一口气都写出来, 取而代之的是, 我将会把它分成几个小的片段, 我相信这样对于你来说会更易于理解.
在我们实现这个方法之前, 先来到 MovieListViewController.m
的最上面, 我们可以看到在实例代码中, 我引入了两个系统的头文件:
#import
#import
OK, 现在我们开始实现这个方法. 先来定义一个变量 index
, 然后来写一个 for...in
循环:
NSInteger index=0;
for (MovieModel *movieModel in self.moviesInfo) {
}
对于每一个电影来说, 我们都会创建一个 CSSearchableItemAttributeSet
对象, 然后我们将会设置一些属性, 这些属性将会在用户搜索的时候, 在 Spotlight
中进行显示. 在我们得示例代码中, 我们来设置一下电影的名称、图片以及描述.
for (MovieModel *movieModel in self.moviesInfo) {
CSSearchableItemAttributeSet *attributeSet = [[CSSearchableItemAttributeSet alloc] initWithItemContentType: (NSString *)kUTTypeText];
// Set the title
attributeSet.title = movieModel.title;
// Set the movie image
NSArray *imagePathParts = [movieModel.imageName componentsSeparatedByString: @"."];
attributeSet.thumbnailURL = [[NSBundle mainBundle] URLForResource: [imagePathParts firstObject] withExtension: [imagePathParts lastObject]];
// Set the description
attributeSet.contentDescription = movieModel.desc;
}
在上面这个代码片段中, 需要注意一下我们是如何设置图片这个属性的, 我们有两种方法可以设置图片: 我们可以提供一个图片的 URL
, 也可以直接使用一个图片的 NSData
对象. 对于我们来说, 最简单的方法就是提供一个图片文件的 URL
.
现在, 我们来设置关键词, 在设置关键词之前, 你应该好好的考虑一下你需要哪些关键词, 因为关键词对于用户的搜索来讲, 是至关重要的. 在示例程序中, 我们会将电影的分类以及演员阵容设置为关键词. 看下面代码片段:
for (MovieModel *movieModel in self.moviesInfo) {
// ....
// Set the keywords
NSMutableArray *keywords = [NSMutableArray array];
NSArray *movieCategroies = [movieModel.category componentsSeparatedByString: @", "];
for (NSString *category in movieCategroies) {
[keywords addObject: category];
}
NSArray *stars = [movieModel.stars componentsSeparatedByString: @", "];
for (NSString *star in stars) {
[keywords addObject: star];
}
attributeSet.keywords = keywords;
}
你应该知道, 电影的分来这个属性在 MoviesData.plist
中被描述为了一个字符串, 每一个分类之间使用了 逗号
进行分割. 所以我们有比较通过 逗号
来将这个字符串分割成一个个单独的分类, 并将它们保存在一个数组中, 然后我们使用了一个 for...in
循环将它们添加到了关键词数组中(keywords). 然后我们利用同样的步骤, 将电影的演员阵容也添加到了关键词数组中.
上面代码片段中, 最重要的一行代码是最后一行: 我们将关键词数组( keywords ) 设置给了 attributeSet
对象的 keywords
属性. 如果你写代码的时候忘记了这一行, 那也就是说, 在用户使用 Spotlight
搜索的时候, 将不会出现任何有关你 App
的内容了, 切记!!!
接下来我们来初始化 CSSearchableItem
对象, 看下面代码片段:
for (MovieModel *movieModel in self.moviesInfo) {
// ....
// Create the searchable item
CSSearchableItem *searchableItem = [[CSSearchableItem alloc] initWithUniqueIdentifier:[NSString stringWithFormat: @"com.liguoan.CoreSpotlightDemo.%@", @(index)] domainIdentifier: @"Movies" attributeSet: attributeSet];
}
上面代码片段中的构造器函数包含了三个参数:
- uniqueIdentifier: 该参数标记了这个
CSSearchableItem
对象在Spotlight
中的唯一标识符. 你可以以你自己喜欢的方式来拼接这个标识符. 不过有一个小细节需要记住: 在示例代码中, 我们为标识符拼接了当前遍历出来电影的下标(index), 因为过一会儿我们需要用到这个下标来展示详情页面. 在标识符中添加一个标志, 来确定当前的数据, 这是一个非常好的方法. 过一会儿你就知道这样做的好处了. - domainIdentifier: 利用这个参数来将你的
CSSearchableItem
对象进行分组. - attributeSet: 这个参数就是刚才我们创建的
CSSearchableItemAttributeSet
对象.
到目前为止, 我们还剩下最后一步操作, 也就是调用 Core Spotlight
相应的 API
来对数据进行索引操作.
for (MovieModel *movieModel in self.moviesInfo) {
// ...
// Index the searchable item
[[CSSearchableIndex defaultSearchableIndex] indexSearchableItems: @[searchableItem]
completionHandler:^(NSError * _Nullable error) {
}];
index++;
}
OK, 我们已经将 - (void)setupSearchableContent
方法的实现部分写完了, 我们需要在 ViewDidLoad
中调用一下该方法:
#pragma mark - Life Cycles
- (void)viewDidLoad {
[super viewDidLoad];
[self setupNavigationBar];
[self setupTableView];
[self setupSearchableContent];
}
接下来运行我们得示例程序, 然后退出, 然后在 Spotlight
中使用我们刚才设置的关键词进行搜索, 我们会发现, 搜索结果已经在 Spotlight
中展示了出来, 当我们点击任意一个搜索结果, 实例程序将会自动被打开, 很帅对吧?
进入详情页面
现在我们已经可以从 Spotlight
中搜索到应用程序中的电影数据了, 不过我们还可以做得更完美. 到目前为止, 点击 Spotlight
搜索结果, 会自动打开示例程序, 并且显示默认的 MovieListViewController
. 但是我们最终的目标是当用户点击搜索结果后自动启动示例程序, 并直接进入详情页面显示电影的详情信息.
这个最终目标听起来可能有些困难和复杂, 不过很快你就会看到, 其实并没有那么复杂, 真的很简单. 我们主要的工作就是复写 UIKit
的 restoreUserActivityState:
方法, 来操作 Spotlight
中选中的搜索结果. 在这个方法中, 我们首先要从 identifier
中获取用户选中电影的下标(还记得在上一部分, 我们创建 identifier
时后面拼接的 index
么?), 然后通过下标, 在 moviesInfo
数组中拿到对应的数据模型, 最后将数据模型传递给 MovieDetailViewController
来进行显示.
restoreUserActivityState:
方法中包含了一个 NSUserActivity
参数. NSUserActivity
对象中包含了一个 userInfo
字典, 这个字典中就包含了在 Spotlight
中选择的搜索结果的 identifier
. 我们看下面代码片段:
- (void)restoreUserActivityState:(NSUserActivity *)activity {
if ([activity.activityType isEqualToString: CSSearchableItemActionType]) {
NSDictionary *userInfo = activity.userInfo;
if (userInfo && userInfo.allKeys.count) {
NSString *movieIdentifier = userInfo[CSSearchableItemActivityIdentifier];
_selectedMovieIndex = [[[movieIdentifier componentsSeparatedByString: @"."] lastObject] integerValue];
[self performSegueWithIdentifier: @"idSegueShowMovieDetails" sender: self];
}
}
}
从上面代码块中可以看到, 我们首先需要判断的是 CSSearchableItemActionType
类型. 实话实说, 在我们这个示例程序中, 判断 CSSearchableItemActionType
类型并不是必须的, 但是在工作的项目中, 假设你的应用程序会操作很多 NSUserActivity
对象, 那么此时千万不要忘记判断这个类型(例如: Handoff
中也会使用到 NSUserActivity
). 在 userInfo
中的 identifier
是一个字符串, 一旦我们获取到了这个字符串, 我们先使用 .
符号将它分割成一个数组, 然后获取数组中的最后一个元素, 也就是所谓的下标(不明白的同学请往回看, 看我们拼接 identifier
的那个代码片段), 我们将下标通过 _selectedMovieIndex
成员变量保存下来, 最后执行 segue
.
现在切换到 AppDelegate.m
文件中, 在这里我们需要实现一个代理方法. 这个代理方法将会在 Spotlight
搜索结果点击后被调用, 在这个方法中, 我们的任务就是调用刚刚实现的这个方法, 来看代码片段:
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler {
if ([userActivity.activityType isEqualToString: CSSearchableItemActionType]) {
UINavigationController *navigationController = (UINavigationController *)self.window.rootViewController;
if ([navigationController isKindOfClass: [UINavigationController class]]) {
UIViewController *bottomViewController = [navigationController.viewControllers firstObject];
[bottomViewController restoreUserActivityState: userActivity];
}
}
return YES;
}
在上面这个代码片段中, 我们同样先判断了 CSSearchableItemActionType
类型, 接下来获取的是 window
的 rootViewController
, 再获取到 MovieListViewController
, 然后调用我们刚才实现的方法. 除了这种方法意外, 你还可以使用 NSNotificationCenter
来达到同样的效果.
OK, 到这里我们的示例程序就完成了, Command + R
将程序跑起来试一下吧.
李国安说:
如果您在文章中看到了错误 或 误导大家的地方, 请您帮我指出, 我会尽快更改
如果您有什么疑问或者不懂的地方, 请留言给我, 我会尽快回复您
如果您觉得本文对您有所帮助, 您的喜欢是对我最大的鼓励
如果您有好的文章, 可以投稿给我, 让更多的 iOS Developer 在这个平台能够更快速的成长