马上着手开发 iOS 应用程序 (九) - 实现导航功能

重要:这是针对于正在开发中的API或技术的预备文档(预发布版本)。苹果提供这份文档的目的是帮助你按照文中描述的方式对技术的选择及界面的设计开发进行规划。这些信息有可能发生变化,因此根据本文档的软件开发应当基于最终版本的操作系统和文档进行测试。该文档的新版本或许会随着API或相关技术未来的发展而进行更新。

翻译自苹果官网:

https://developer.apple.com/library/ios/referencelibrary/GettingStarted/DevelopiOSAppsSwift/Lesson8.html#//apple_ref/doc/uid/TP40015214-CH16-SW1

本课中,使用导航控制器和 segues 为 app 创建导航流。最后会完成一个完整的导航方案。样子如下:

[图片上传失败...(image-421b09-1608214786914)]

学习目标

本课的最后,你将能够:

  • 在 storyboard 中嵌入视图控制器到导航控制器里面。
  • 在视图控制器之间创建 segues。
  • 使用属性检查器编辑 segue 的属性。
  • 使用 prepareForSegue(_:sender:) 方法在视图控制器间传递数据
  • 操作一个 unwind segue
  • 使用堆栈视图来创建强大灵活的布局

添加一个 segue 导航

数据像预期一样显示了,是时候提供从食物列表导航到食物场景的方式了。场景间的过渡被称为 segues。

在创建 segue 前,你需要配置场景。首先,把表格视图控制器放到导航控制器里面。导航控制器用于在一系列的视图控制器间前后过渡。视图控制器集合由一个叫导航栈的导航控制器管理着。第一个添加到栈中的控制器成为 root view controller 并且永远也不会从导航栈中弹出。

添加导航控制器
  1. 打开 Main.storyboard。

  2. 通过点击场景 dock 选中表格视图控制器。

    [图片上传失败...(image-472cc4-1608214786914)]

  3. 选中后,选择 Editor > Embed In > Navigation Controller。

    Xcode 往 storyboard 中添加新的导航控制器,设置它为 storyboard entry point,并在导航控制器和表格视图控制器间创建关联。

    [图片上传失败...(image-e3745f-1608214786915)]

在画板中,连接控制器间的图标是 root view controller relationship。表格视图控制器是导航控制器的根视图控制器。storyboard entry point 设置为导航控制器因为它现在是表格视图控制器的容器了。

你或许注意到 table view 的顶部有个条形控件。这就是导航栏。在导航栈中每个控制器都得到了一个导航栏,它可以包含控件用于向前和向后导航。下一步,往导航栏添加按钮用于食物场景间过渡。

检验:运行 app。在表格视图的上面你应该看到额外的控件。这就是导航控制器提供的导航栏。它扩展状态栏为它自己的背景,所以状态栏不再覆盖你的内容了。

[图片上传失败...(image-b3d88-1608214786915)]

为场景配置导航栏

现在,给导航栏添加一个标题(食物列表)和一个按钮(用于添加食物)。导航栏从当前显示的视图控制器中得到标题 - 它们自己并没有标题。使用表格视图控制器的 navigation item 设置标题而不是直接设置到导航栏上。

在食物列表中配置导航栏
  1. 双击食物列表场景中的导航栏。

    [图片上传失败...(image-646c8b-1608214786915)]

    光标出现了,让你输入文字。

  2. 输入 Your Meals 然后按回车来保存。

    [图片上传失败...(image-832eb8-1608214786915)]

  3. 打开对象库。(选择 View > Utilities > Show Object Library。)

  4. 在对象库中找到 Bar Button 对象。

  5. 从列表中拖动这个 Bar Button 对象到食物列表场景的导航栏上。
    当拖动 bar button item 时候出现一个按钮叫做 Item 。

    [图片上传失败...(image-93ceee-1608214786915)]

  6. 选择 bar button item 然后打开属性检查器。

  7. 从 System Item 选项旁边的弹出菜单中选择 Add。按钮修改为一个 + 按钮。

    [图片上传失败...(image-960b61-1608214786915)]

检验:运行 app。导航栏应该有个标题并显示一个加号(+)按钮。按钮不会做任何事。稍后会修复这个问题。

[图片上传失败...(image-809c46-1608214786915)]

你希望 + 按钮打开场景界面,所以让按钮触发一个 segue 到这个场景并打开它。

配置食物场景中的加号按钮
  1. 在画板中,选择加号按钮(+)。

  2. 按住 Control 从按钮拖动到食物场景中。

    [图片上传失败...(image-f9431e-1608214786915)]

    在拖动结束的地方出现一个标题 Action Segue 的快捷菜单。

    [图片上传失败...(image-56ef72-1608214786915)]

    当用户点击加号按钮时候,Action Segue 菜单让你选择使用哪种 segue 从食物列表导航到新的食物视图控制器中。

  3. 选择 Action Segue 菜单中的 show。

Xcode 设置 show segue 并配置食物场景在导航控制器中显示 - 在 Interface Builder 中看到的导航栏。

[图片上传失败...(image-c4f2ef-1608214786915)]

检验:运行 app。点击加号按钮并从食物列表场景导航到食物场景中。因为你正在使用一个 show segue 的导航控制器。这意味着你可以在食物场景中点击返回按钮来返回食物列表界面。

[图片上传失败...(image-19bee4-1608214786915)]

使用推荐的 show segue 最终产生 push 样式的导航 - 但这不是在添加一份食物的时候应该做的。Push 导航被设计用来给用户提供更多信息。而添加一份食物,在另一方面,是个模态操作-用户执行的动作是完整且独立的,之后从场景中返回主导航。为这类场景合适的呈现方式就是 modal segue。

取代删除存在的 segue 并创建一个新的,在属性检查器中简单修改 segue 的样式。因为对于大多数 storyboard 中选中的控件,可以使用属性检查器去编辑一个 segue 的属性。

修改 segue 样式
  1. 从食物列表场景选择 segue 到食物场景。

    [图片上传失败...(image-abbec9-1608214786915)]

  2. 在属性检查器中 Segue 选项旁边的弹出菜单中选择 Present Modally。

  3. 在属性检查器的 Identifier 区域旁边输入 AddItem。回车确认。
    之后,需要这个标识来识别 segue。

一个模态视图控制器不需要添加到导航栈中,所以它不会从食物列表导航控制器中得到一个导航栏。然而,你想要保持导航栏来给用户提供视觉一致性。为了在模式弹出时给食物场景一个导航栏,把它嵌入到它自己的导航控制器。

给食物场景添加导航控制器
  1. 点击它的场景 dock 选中食物场景。

    [图片上传失败...(image-4555e4-1608214786915)]

  2. 选中后,选择 Editor > Embed In > Navigation Controller。

像之前一样,Xcode 添加导航控制器并在食物场景顶部显示导航栏。下一步,配置导航栏给场景添加一个标题和连个按钮,Cancel 和 Save。稍后,将为这些按钮连接动作。

[图片上传失败...(image-657c89-1608214786915)]

在食物场景中配置导航栏
  1. 双击食物场景中的导航栏。

    [图片上传失败...(image-3ac466-1608214786915)]

    出现一个光标,让你输入文本。

  2. 输入 New Meal 并回车确认保存。

  3. 从对象库中拖动一个 Bar Button Item 对象到食物场景导航栏的左边。

  4. 在属性检查器的 System Item 处,选择 Cancel。
    按钮文本修改为 Cancel。

[图片上传失败...(image-e112f2-1608214786915)]

  1. 从对象库中拖动另一个 Bar Button Item 对象到食物场景导航栏的最右边。

  2. 在属性检查器的 System Item,选择 Save。
    按钮文本修改为 Save。

    [图片上传失败...(image-2e3eab-1608214786915)]

检验:运行 app。点击 Add 按钮。你仍然能够看到食物场景,但是不再有导航返回食物列表的按钮了 - 相应的,你会看到你添加的两个按钮, Cancel 和 Save。这些按钮还没有关联任何动作,点击它们,但是并没有做什么。稍后配置按钮来保存或取消添加一个新的食物并让用户返回食物列表。

[图片上传失败...(image-668f7f-1608214786915)]

使用自动布局 (Auto Layout)完成界面

从你构建原始界面有一段时间了,从那时起有好多事情已经改变了。此刻,你不需要对布局做任何改变,所以是时候使用自动布局确保一切看起来很棒。

为了做这些,对你的堆栈视图做一些调整。

更新堆栈视图的布局
  1. 在食物场景中,点击淡蓝色区域选中堆栈视图。

    [图片上传失败...(image-c64a3a-1608214786915)]

    也可以在大纲视图中选择堆栈视图。

  2. 在画板的右下角,打开 Resolve Auto Layout Issues 菜单。

    [图片上传失败...(image-8b3dff-1608214786915)]

  3. 在 Selected Views 下面,选择 Update Constraints。

    [图片上传失败...(image-35cdb7-1608214786915)]

    控件还在原位置,但是堆栈视图现在被导航栏约束而不是视图顶部边界。

食物场景约束和界面应该像这样:

[图片上传失败...(image-1761dc-1608214786915)]

检验:运行你的 app。文本框,image view 和评分控件应该接近导航栏了。

在食物列表中保存一份新的食物

下一步为 FoodTracker app 添加的功能就是让用户有能力添加一份新的食物。当用户在食物场景中输入食物的名字,评分和照片并点击保存按钮,MealViewController 配置个带有合适信息的 Meal 对象并传递给 MealTableViewController 用来显示在食物列表中。

通过添加一个 Meal 属性到 MealViewController 开始。

给 MealViewController 添加一个 Meal 属性
  1. 打开 MealViewController.swift。

  2. 在 MealViewController.swift 的 ratingControl outlet 下面,添加如下属性:

     /*
     This value is either passed by `MealTableViewController` in `prepareForSegue(_:sender:)`
     or constructed as part of adding a new meal.
     */
     var meal: Meal?
    

    在 MealViewController 中定义了一个可选的 Meal 属性,这意味着在有些时候,它或许是 nil。

连接 Save 按钮到 MealViewController 代码
  1. 打开你的 storyboard。

  2. 点击 Xcode 工具栏的 Assistant 按钮来打开辅助编辑器。

    [图片上传失败...(image-e4c083-1608214786915)]

  3. 如果想要更多空间来工作,点击 Xcode 工具栏的 Navigator 和 Utilities 按钮收缩项目导航和实用工具区。

    [图片上传失败...(image-8d6ded-1608214786915)]

  4. 在你的 storyboard 中,选择 Save 按钮。

  5. 按住 Control 从画板的 Save 按钮拖动到右边编辑器中的代码,在 ratingControl 属性下面一行停止拖动。

    [图片上传失败...(image-d15130-1608214786915)]

  6. 在出现的对话框中,输入名字:saveButton。
    忽略剩下的选项。你的对话框应该像这样:

    [图片上传失败...(image-ecac67-1608214786915)]

  7. 点击 Connect。

你或许有种识别 Save 按钮的办法。

创建一个 Unwinde Segue

当用户点击 Save 和 Cancel 按钮时,传递 Meal 对象给 MealTableViewController。就是从显示食物场景切换为食物列表。

为了完成这些,使用 unwind segue。它让用户返回到上一个或者更多的视图控制器的实例。使用 unwind segues 来实现反向导航。

当 segue 触发了,它给你提供一个添加你自己代码的地方。这个方法叫做 prepareForSegue(_:sender:),它给你一个机会储存数据和在源视图控制器(segue 来源于这个视图控制器)做一些必要的清理工作。即将在 MealViewController 中实现这个方法。

在 MealViewController 中实现 prepareForSegue(_:sender:) 方法
  1. 点击 Standard 按钮返回标准编辑器。

    [图片上传失败...(image-fcfe56-1608214786915)]

  2. 打开 MealViewController.swift。

  3. 在 MealViewController.swift 的 // MARK: Actions 区域,添加如下注释:

     // MARK: Navigation
    

    这个注释让你(和其他任何读你代码的人)知道方法是关联 app 的导航流。

  4. 在注释的下面,添加方法主体:

     // This method lets you configure a view controller before it's presented.
     override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
     }
    
  5. 在 prepareForSegue(_:sender:) 方法中,添加如下 if 语句:

     if saveButton === sender {
     }
    

    代码通过使用恒等运算符(===)来检查 saveButton 关联的 outlet 和 sender 对象是否相同的对象。如果是,if 语句就会执行。

  6. 在 if 语句里面,添加如下代码:

     let name = nameTextField.text ?? ""
     let photo = photoImageView.image
     let rating = ratingControl.rating
    

    注意 name 行的 ?? 用于在可选无值时返回一个默认的值。这里展开 nameTextField.text 返回的可选 String(它是可选的因为文本框或许没有文字),如果是不合法的字符串会返回 nil。然后使用空字符串("")代替返回。

  7. 在 if 语句中,添加如下代码:

     // Set the meal to be passed to MealTableViewController after the unwind segue.
     meal = Meal(name: name, photo: photo, rating: rating)
    

    代码在 segue 执行前给 meal 属性配置合适的值。

你的 prepareForSegue(_:sender:) 方法应该像这样:

    // This method lets you configure a view controller before it's presented.
    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        if saveButton === sender {
            let name = nameTextField.text ?? ""
            let photo = photoImageView.image
            let rating = ratingControl.rating
            
            // Set the meal to be passed to MealTableViewController after the unwind segue.
            meal = Meal(name: name, photo: photo, rating: rating)
        }
    }

下一步创建 unwind segue 的操作就是添加一个动作方法到目标视图控制器( segue 要跳过去的视图控制器)。这个方法应该被 IBAction 属性标记并拥有一个 segue (UIStoryboardSegue) 的参数。因为想要返回食物列表场景,使用这种方式添加动作方法。

在这个方法中,编写逻辑来添加新的食物(从源视图控制器 MealViewController 中传递)到食物列表数据中并在 table view 中添加新的一行。

为 MealTableViewController 添加一个动作方法
  1. 打开 MealTableViewController.swift。

  2. 在 MealTableViewController.swift 的大括号 } 前面,添加如下方法:

     @IBAction func unwindToMealList(sender: UIStoryboardSegue) {
     }
    
  3. unwindToMealList(_:) 动作方法中,添加如下 if 语句:

     if let sourceViewController = sender.sourceViewController as? MealViewController, meal = sourceViewController.meal {
     }
    

    这个 if 条件语句里面有许多门道。

    代码使用可选类型转换操作符(as?)来尝试向下转换 segue 的视图控制器到 MealViewController 类型。你需要转换因为 sender.sourceViewController 是 UIViewController 的类型。但是你需要使用 MealViewController。

    操作符返回可选值,当下转不可能的时候变成 nil。如果下转成功,代码将视图控制器赋给局部常量 sourceViewController,检查 sourceViewController 中的 meal 属性是否为 nil。如果 meal 属性不为 nil,代码分配属性的值给本地常量 meal 并执行 if 语句。

  4. 在 if 语句中,添加如下代码:

     // Add a new meal.
     let newIndexPath = NSIndexPath(forRow: meals.count, inSection: 0)
    
  5. 在 if 语句前一行的代码下面,添加如下代码:

     meals.append(meal)
    

    添加新的一份食物到列表中。

  6. 在 if 语句的前一行代码的下面,添加如下代码:

     tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom)
    

    往 tableView 中插入新的一行单元格。.Bottom 动画选项说明插入的行是从底部显示出来的。

稍后将完成这个方法更深层次的实现,但是现在,unwindToMealList(_:) 方法应该像这样:

    @IBAction func unwindToMealList(sender: UIStoryboardSegue) {
        if let sourceViewController = sender.sourceViewController as? MealViewController, meal = sourceViewController.meal {
            // Add a new meal.
            let newIndexPath = NSIndexPath(forRow: meals.count, inSection: 0)
            meals.append(meal)
            tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom)
        }
    }

现在你需要创建 unwind segue 来触发这个动作方法。

连接 Save 按钮和 unwindToMealList 动作方法
  1. 打开 storyboard。

  2. 在画板中,按住 Control 从 Save 按钮拖动到食物场景顶部的 Exit 选项中。

    [图片上传失败...(image-7fce92-1608214786915)]

    在拖拽结束的位置弹出一个菜单。

    [图片上传失败...(image-5b553-1608214786915)]

  3. 从快捷菜单中选择 unwindToMealList:。
    现在,当用户点击 Save 按钮,在 unwindToMealList(_:) 动作方法调用过程中,他们导航返回到食物列表场景。

检验:运行 app。现在当你点击 + 按钮,创建一份新的食物,然后点击 Save,你应该在你的食物列表中看到这份新的食物了。

重要提示:

如果你在快捷菜单中没有看到 unwindToMealList 方法,确保方法有正确的标识。@IBAction func unwindToMealList(sender: UIStoryboardSegue)

当用户未输入食物名不能保存

如果用户尝试保存一份没有名字的食物会导致什么后果?因为 MealViewController 中的 meal 属性是可选的所以当没有名字的时候构造失败,Meal 对象不会创建和添加到食物列表中,这正是我们预期发生的。但是可以更进一步防止这种意外,当用户正在输入食物的名字禁用保存按钮,在关闭键盘前检查他们是否指定了正确的名字。

当没输入名字时禁用保存按钮
  1. 在 MealViewController.swift 中,找到 // MARK: UITextFieldDelegate 区域。
    你可以使用 functions menu 快速跳到那里,它会在你点击编辑区顶部的名字时候出现。

  2. 在这个区域,添加另一个 UITextFieldDelegate 方法:

     func textFieldDidBeginEditing(textField: UITextField) {
         // Disable the Save button while editing.
         saveButton.enabled = false
     }
    

    当编辑开始时 textFieldDidBeginEditing 方法会被调用。这时禁用保存按钮。

  3. 在 textFieldDidBeginEditing(_:) 方法的下面,添加另一个方法:

     func checkValidMealName() {
         // Disable the Save button if the text field is empty.
         let text = nameTextField.text ?? ""
         saveButton.enabled = !text.isEmpty
     }
    

    这是个帮助方法用来当文本框是空时禁用保存按钮。

  4. 找到 textFieldDidEndEditing(_:) 方法:

     func textFieldDidEndEditing(textField: UITextField) {
     }
    

    此刻实现应该是空的。

  5. 添加这些行代码:

     checkValidMealName()
     navigationItem.title = textField.text
    

    第一行调用 checkValidMealName() 来检查文本框中是否有文本,如果有启用保存按钮。第二行设置场景的标题为它的文本。

  6. 找到 viewDidLoad() 方法:

     override func viewDidLoad() {
         super.viewDidLoad()
         
         // Handle the text field’s user input through delegate callbacks.
         nameTextField.delegate = self
     }
    
  7. 在实现部分添加 checkValidMealName() 的调用确保禁用了保存按钮直到用户输入正确的名字:

     // Enable the Save button only if the text field has a valid Meal name.
     checkValidMealName()
    

viewDidLoad() 方法应该像这样:

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Handle the text field’s user input through delegate callbacks.
        nameTextField.delegate = self
        
        // Enable the Save button only if the text field has a valid Meal name.
        checkValidMealName()
    }

textFieldDidEndEditing(_:) 方法应该像这样:

    func textFieldDidEndEditing(textField: UITextField) {
        checkValidMealName()
        navigationItem.title = textField.text
    }

检验:运行 app。现在当你点击 + 按钮,保存按钮被禁用直到你输入一个正确的(非空)的食物名字并关闭键盘了。

[图片上传失败...(image-39005a-1608214786915)]

Cancel 添加新的食物

用户或许决定取消添加新的食物,然后不保存就返回食物列表。为了做到这些,实现 Cancel 按钮的功能。

创建和实现一个取消动方法
  1. 打开 storyboard。

  2. 点击 Xcode 工具栏的 Assistant 按钮打开辅助编辑器。

    [图片上传失败...(image-ab9791-1608214786915)]

  3. 在 storyboard 中,选择 Cancel 按钮。

  4. 按住 Control 从画板的 Cancel 按钮拖动到右边编辑器的代码中,在 MealViewController.swift 的 // MARK: Navigation 注释下面一行停止拖动。

    [图片上传失败...(image-7cd3e6-1608214786915)]

  5. 在弹出的对话框的 Connection 区域选择 Action。

  6. 输入名字:cancel。

  7. 类型选择 UIBarButtonItem。
    忽略剩下的选项。你的对话框应该像这样:

    [图片上传失败...(image-d72a07-1608214786915)]

  8. 点击 Connect。
    Xcode 添加必要的代码到 MealViewController.swift 中来设置这个动作。

     @IBAction func cancel(sender: UIBarButtonItem) {
     }
    
  9. 在 cancel(_:) 方法中,添加如下行代码:

     dismissViewControllerAnimated(true, completion: nil)
    

    代码在没有保存任何信息的情况下关闭食物场景。关闭后,显示食物列表。

cancel(_:) 动作方法应该像这样:

    @IBAction func cancel(sender: UIBarButtonItem) {
        dismissViewControllerAnimated(true, completion: nil)
    }

检验:运行 app。现在点击 + 之后点击 Cancel 而不是 Save,你应该导航返回到食物列表但你并没有添加一份新的食物。

注意:

为了看到本课的完整示例项目,下载文件并在 Xcode 中查看它。

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

推荐阅读更多精彩内容