如何使用 Core Spotlight

前言:
先放出原文地址: AppCoda, 支持原创作品哈.
本文为上面这篇文章的译文, 原文中使用 Swift 语言, 本文使用 Objective-C 语言, 其中大部分内容为翻译(其中有些没用的废话就没翻译), 有一部分自己的理解. 示例代码也有修改, 和原文中有出入. 喜欢看英文文档的小伙伴请直接查看原文.

伴随着每一个新版本 iOS 的更新, 苹果都会给全世界的开发者们带来一些新的技术, 当然 iOS9 也不会违背这个传统. 其中之一就是 Core Spotlight 框架, 它包含了许多很棒的 APIs, 一旦开发者们将它们集成在自己的应用中, 就可以让他们的应用提升到一个新的高度.

Core Spotlight 框架是苹果提供的 APIs 集合中的一部分, 被称之为 Search APIs. 它可以帮助你很有效的拉近与用户之间的距离, 让用户更容易访问你的应用程序. 除了 Core Spotlight 之外, iOS9 中其它的搜索功能还包括:

  1. NSUserActivity: 新的方法和属性(用来保存 App 当前状态, 并在以后恢复该状态).
  2. Web Markup: 可以让你在设备中搜索 Web 中的内容.
  3. Universal Links: 通过 Web 直接开启应用程序.

在本篇文章中, 我们不会去讨论上面这三个搜索功能, 我们只专注于 Core Spotlight 框架. 首先我们来看一下 Core Spotlight 到底是什么.

图片出自: AppCoda

Core Spotlight Framework 可以让你 App 中的内容在 Spotlight 中搜索到, 并且将相关的搜索结果展现给用户, 并且允许用户和搜索的结果进行交互. 当用户选择了其中一个搜索的结果后, 不但可以自动的打开你的应用程序, 同时还可以跳转到指定的页面来查看详细的内容.

从开发者的角度来看, 集成 Core Spotlight 框架并使用它的 APIs 并不复杂. 通过接下来的教学, 你会发现必要的代码可能也就只有几行就够了. 其中最核心的处理过程就是开发者需要让 iOS 对应用程序中的数据进行索引操作.

本篇教学主要是针对 Core Spotlight 框架, 但是我并不打算在本篇文章中对一些细节部分进行讲解. 如果你有幸去学习一些我个人认为很棒的东西, 那就请继续阅读本篇文章. 我很自信的告诉你, 在你阅读完本篇文章之后, 你会发现集成 Core Spotlight 框架, 以及使用 Spotlight 搜索你应用中的内容是多么的简单.

关于 Demo

和往常一样, 我们会通过一个实例程序来进行讲解和学习. 这一次我们的重点内容是在 App 内填充一些数据, 并允许这些数据在 Spotlight 中进行搜索. 除了重点之外, 我们还需要更多的了解一下实例程序.

实例程序的主要目的是为了展示一些电影相关的数据, 例如: 简介、导演、评分等. 所有的数据都会用 TableView 来进行展示, 当选中一个电影, 会进入到一个新的详情页面来进行展示. 基本上就是这些内容, 我们通过这些数据以及功能来学习 Core Spotlight 是如何工作的. 实例程序中的数据来自这里.

通过下面这张动图, 来感受一下实例程序.


图片出自: AppCoda

在本篇教学中, 我们有两个目标: 最重要的一个目标是, 我们需要让示例程序中的所有电影数据都可以在 Spotlight 中通过关键词来进行搜索. 当然, 设置关键词也是我们的任务之一.

当用户点击了一条搜索结果, 应用程序将会自动打开, 接下来就是我们的第二个目标, 如果我们不做任何处理, 那么默认的视图控制器将会被加载并呈现在用户眼前, 也就是我们的首页(电影列表的那个页面). 然而当我们站在用户体验的角度来思考这个问题, 其实这样并不是一个完美的解决方案. 完美的解决方案是, 当用户选择了一个搜索结果, 我们应该启动应用程序, 并给用户展示对应电影的详情数据, 这就是我们得第二个目标. 简单来说, 我们不仅要做到在 Spotlight 中所搜到应用中的电影, 我们还要在用户选择了某一个电影后, 开启应用程序并进入到详情页面来显示电影的详情信息. 看下面这张动图, 你就明白了:

图片出自: AppCoda

为了不浪费时间, 你可以在这里下载实例程序. 在实例程序中, 你将会看到以下的内容:

  • UI 以及 IBOutlet 已经完成.
  • 最简单的实现了 UITableView.
  • 所有的电影数据都在 .plist 文件中, 另外还包含了对应的图片(一共5个).

用一张图片来解释, 下图中展示了 .plist 文件中包含数据以及数据的结构.

图片出自: AppCoda

在学习 Core Spotlight 之前, 先来完成两个小任务:

  1. 加载数据, 并填充在 UITableView 中.
  2. Detail View Controller 中显示选中的电影详情.

在实例工程的初始阶段, 如果我实现了上面这两个功能会使我们更快的进入 Core Spotlight 的学习阶段, 但是我并没有这么做, 原因很简单: 我相信通过这个过程, 你会更容易理解这些数据是如何从一些普通的数据变为 Spotlight 可搜索到的数据. 不用担心, 实现上面两个功能很简单, 我们会很快完成的.

加载、展示实例数据

OK, 我们现在就可以开始了, 假设你现在已经下载好了实例程序, 并且已经对 .plist 文件中的电影数据有了一定的了解. 我们的第一个任务是将 .plist 文件中的数据加载到数组中, 并将它们填充到 TableView 中.

我们直接开始写代码, 打开 MovieListViewController.m 文件, 先来定义一个数据源数组:

@property (nonatomic, strong) NSMutableArray<MovieModel *> *moviesInfo;

所有的电影数据, 都会被加载到这个数组中. 每一个电影数据都用一个 MovieModel 来表示. 这个数据模型的逻辑已经写完了, 有兴趣的同学可以看看, 本人实在是懒得导入 MantleYYModel 框架了, 所以就直接用 objectForKey 解析数据了. 哈哈! 尴尬!

.plist 数据转数据模型的代码已经封装在 MovieModel 中了, 在 MovieListViewController.m 中添加以下代码:

#pragma mark - Lazy Load
- (NSMutableArray<MovieModel *> *)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 的样式.

Movie 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. 其次是利用数据模型的内容对 MovieDetailViewControllerUI 控件进行数据的填充.

MovieDetailViewController.h 文件该为下面代码块显示的一样:

@class MovieModel;

@interface MovieDetailViewController : UIViewController

@property (nonatomic, strong) MovieModel *movieModel;

@end

接下来我们暂时先回到 MovieListViewController.m 文件中, 来看一下当选中了一部电影之后, 我们需要做哪些事情. 当点击事件发生之后, 我们希望知道点击的是第几部电影, 并且获取到数据源数组中对应的数据模型, 并将它传递给 MovieDetailViewController. 利用 TableView 的代理方法来获取对应的数据模型很简单, 但是我们需要将它保存下来, 所以在 MovieListViewController.m 中我们需要再定义一个成员变量:

@interface MovieListViewController () <UITableViewDelegate, UITableViewDataSource> {
    
    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(PushMovieDetailViewController). 然而仅做这三件事还是不够的, 因为我们还没有从数据源数组中获取到对应的数据模型, 并且我们还没有向 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];
    }
}

非常简单, 我们通过 seguedestinationViewController 属性来获取 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 SpotlightAPI 来对相应的数据进行索引操作, 这样一来, 当用户使用 Spotlight 进行搜索时, 就能搜索到相应的数据了. 至于哪些数据可以被搜索到, 不是应用程序决定的, 也不是 Core Spotlight 决定的, 而是我们决定的, 所以我们有责任为 Core Spotlight 提供数据, 告知哪些数据是允许被搜索的.

所有允许被搜索到的数据, 都被描述成为一个 CSSearchableItem 对象, 将这些对象放到一个数组中, 递交给 Core Spotlight 框架中相应的 API 来进行索引操作. 一个 CSSearchableItem 中包含了一系列的属性用来描述一个允许被搜索的数据, 例如: 电影名称、图片、描述、搜索关键词等... 在 CSSearchableItem 中所有的这些属性都被描述成为了一个 CSSearchableItemAttributeSet 对象, 该对象中提供了这些我们可能需要用到的属性. 作为参考, 在这里给你一个官方文档的链接进行查看.

对数据进行索引操作是最后一步, 也是必须要做的一步. 一般包含以下几个步骤:

  1. CSSearchableItemAttributeSet: 设置搜索对象的属性.
  2. CSSearchableItem: 为每一个搜索数据创建一个 CSSearchableItem 对象, 并关联第一步中生成的属性对象.
  3. 将所有的 CSSearchableItem 对象放入到数组中.
  4. 将数组递交给 Core Spotlight 对应的 API 对数据进行索引操作.

我们接下来会一步一步的根据上面这四个步骤进行操作. 不过在这之前我们需要在 MovieListViewController.m 中新增一个方法叫 - (void)setupSearchableContent. 当我们完成这个方法的实现部分之后, 你会发现其实很简单. 不过我不会把所有的实现代码一口气都写出来, 取而代之的是, 我将会把它分成几个小的片段, 我相信这样对于你来说会更易于理解.

在我们实现这个方法之前, 先来到 MovieListViewController.m 的最上面, 我们可以看到在实例代码中, 我引入了两个系统的头文件:

#import <CoreSpotlight/CoreSpotlight.h>
#import <MobileCoreServices/MobileCoreServices.h>

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<NSString *> *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<NSString *> *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 中展示了出来, 当我们点击任意一个搜索结果, 实例程序将会自动被打开, 很帅对吧?

图片出自: AppCoda

进入详情页面

现在我们已经可以从 Spotlight 中搜索到应用程序中的电影数据了, 不过我们还可以做得更完美. 到目前为止, 点击 Spotlight 搜索结果, 会自动打开示例程序, 并且显示默认的 MovieListViewController. 但是我们最终的目标是当用户点击搜索结果后自动启动示例程序, 并直接进入详情页面显示电影的详情信息.

这个最终目标听起来可能有些困难和复杂, 不过很快你就会看到, 其实并没有那么复杂, 真的很简单. 我们主要的工作就是复写 UIKitrestoreUserActivityState: 方法, 来操作 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 类型, 接下来获取的是 windowrootViewController, 再获取到 MovieListViewController, 然后调用我们刚才实现的方法. 除了这种方法意外, 你还可以使用 NSNotificationCenter 来达到同样的效果.

OK, 到这里我们的示例程序就完成了, Command + R 将程序跑起来试一下吧.

图片出自: AppCoda


李国安说:

如果您在文章中看到了错误 或 误导大家的地方, 请您帮我指出, 我会尽快更改

如果您有什么疑问或者不懂的地方, 请留言给我, 我会尽快回复您

如果您觉得本文对您有所帮助, 您的喜欢是对我最大的鼓励

如果您有好的文章, 可以投稿给我, 让更多的 iOS Developer 在简书这个平台能够更快速的成长

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容

  • 作者:AppCoda,原文链接,原文日期:2015-12-22译者:BigbigChai;校对:walkingwa...
    梁杰_numbbbbb阅读 916评论 1 6
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,806评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • 惯例,啰嗦一番。 嗯,这副画其实很早之前就开始画了的, 但途中有很多小插曲啊,把彩铅借给别人了,遇上部门游了,以及...
    betteryr阅读 329评论 4 5
  • 文/孤鸟差鱼 一无所有的人 还在摆阔 你别叫醒他
    孤鸟差鱼阅读 424评论 3 6