Start Developing iOS Apps (Swift)->实现编辑和删除行为

在本课中,你要关注两个功能——允许用户编辑和删除FoodTracker应用中的菜品。

学习目标

在本课结束的时候,你将能够:

  • 区分压栈(push)导航和模态(modal)导航
  • 基于视图控制器的呈现方式移除该控制器
  • 使用segue identifier(segue 标识符)确定哪个segue正在执行
  • 启用table view controller的编辑模式。

启用现有菜品的编辑

当前,FoodTracker应用给用户添加新菜品到菜品列表的功能。在本课中,你将启用已有菜品的编辑。

当用户点击一个在表格场景中的菜品时,你将在详情场景显示这个菜品。用户能够改变这个菜品。如果他们点击Save按钮,你将同时更新菜品的数据以及它在菜品列表中的呈现。注意,应用没有保存模型数据。每当应用重启,它总是初始化样本数据。不过,当应用运行的时候,用户仍能修改数据。

通过设置菜品列表和菜品详情场景之间的segue开始。

配置table view cell

  1. 如果打开了助理编辑器,回到标准编辑器。


    image: ../Art/standard_toggle_2x.png
  2. 打开storyboard。
  3. 在画布上,选择在菜品列表场景中的table view cell。
  4. 按住Control键拖拽这个cell到菜品详情场景。


    image: ../Art/IEDB_drag_tabletomealscene_2x.png

    image: ../Art/IEDB_seguemenu_2x.png
  5. 在弹出的 选择segue菜单 选择Show。这个会使导航控制器将菜品详情场景压栈到导航控制器。
  6. 把菜品列表场景和菜品详情场景中键的导航控制器向下拉,就能看到这个新segue。


    image: ../Art/IEDB_drag_navcontroller_2x.png

    如果愿意,你能够使用在画布底部的缩放命令来缩放。

  7. 在画布上,选择新添加的segue。


    image: ../Art/IEDB_selectsegue_2x.png
  8. 在Attributes inspector,在Identifier键入ShowDetail。按下回车键。


    image: ../Art/IEDB_inspector_attributes_segue_2x.png
    image: ../Art/IEDB_inspector_attributes_segue_2x.png

    当在代码中引用segue的时候要使用这个标识符。

当用户点击一个菜品列表中的行时,这个segue被触发。这个segue把菜品详情场景的视图控制器压栈到包含菜品列表场景的导航栈上。应用会动画的方式转换这两个场景。

检查点:运行应用。在菜品列表上点击一个菜品。应用导航到菜品详情场景。注意:不像上一课中将的一样系统会自动提供返回按钮。这是因为你在导航栏的左侧添加了自己的栏按钮。

image: ../Art/IEDB_firstcheckpoint_2x.png

创建新菜品和编辑现有菜品是非常相似的操作。因为,完成这两个任务你使用的是同一个界面。当然,你需要为场景的呈现和它的行为做一些修改。当用户添加新菜品和编辑一个现有菜品时,你需要一种方法来识别。

回想一下,prepare(for:sender:)方法是在任何segue执行之前被调用的。你可以使用这个方法来识别哪个segue正在使用,并且在菜品详情场景中显示合适的信息。你是基于你给segue分配的标识符来区分它们的:当添加新菜品时是AddItem,而当编辑已有菜品的时是ShowDetail。

辨别哪个segue正在使用

  1. 打开MealTableViewController.swift
  2. 在文件的顶部,紧跟着导入UIKit下面,导入统一日志系统:
    import os.log
  1. 在MealTableViewController.swift中,找到prepareForSegue(_:sender:)并取消注释。
    然后,你的模版实现方法看上去是这样的:
        //MARK: - Navigation
         
        // In a storyboard-based application, you will often want to do a little preparation before navigation
        override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
            
            // Get the new view controller using segue.destinationViewController.
            // Pass the selected object to the new view controller.
        }

因为MealTableViewController是UITableViewController的子类,所以模版实现带有prepare(for:sender:)骨架。

  1. 删除两行注释,并且用调用超类的实现来替代它们。
        super.prepare(for: segue, sender: sender)
  1. 在调用super.prepare(for:sender:)后,添加下面的switch语句:
        switch(segue.identifier ?? "") {
            
        }

Switch语句考虑一个值,并将它和几种可能的匹配模式进行比较。然后基于第一个匹配成功的模式,执行合适的代码块。当要在多个选项中进行选择的时候使用switch语句代替if语句。
上面的代码检查segue的标识符。如果标识符为nil, nil-coalescing 运算符(??)空字符串(“”)代替它。在本例中你不需要处理多个选项,这件化了switch语句的逻辑。

  1. 添加AddItem分支到switch。
        case "AddItem":
        os_log("Adding a new meal.", log: OSLog.default, type: .debug)

如果用户添加一个项目到菜品列表,你不需要改变菜品详情场景的外观。只是在控制台记录一条简单的调试信息。如果你调试代码,这将帮助你跟踪应用的工作流。

  1. 添加ShowDetail分支到switch。
        case "ShowDetail":
        guard let mealDetailViewController = segue.destination as? MealViewController else {
            fatalError("Unexpected destination: \(segue.destination)")
        }
         
        guard let selectedMealCell = sender as? MealTableViewCell else {
            fatalError("Unexpected sender: \(sender)")
        }
         
        guard let indexPath = tableView.indexPath(for: selectedMealCell) else {
            fatalError("The selected cell is not being displayed by the table")
        }
         
        let selectedMeal = meals[indexPath.row]
        mealDetailViewController.meal = selectedMeal

如果你正在编辑已有册菜品,你需要在菜品详情场景中显示这个菜品的数据。这个代码以获取目标视图控制器开始,然后选择菜品cell,然后获取选中cell的index path。guard 语句检查所有执行的降级工作、以及所有包含非空值的可选类型。这里,guard语句只是进行简单的合理检查。如果你的storyboard被正确的设置,guard语句将不会失败。
一旦你又了index path,你可以查找这个路径上的菜品对象,并传递给目标视图控制器。

  1. 添加默认分支。
        default:
        fatalError("Unexpected Segue Identifier; \(segue.identifier)")

如果你的storyboard被正确设置,这个默认分支永远不会被执行。但是,如果你稍后从你的菜品场景添加另外的segue而忘了更新 prepare(for:sender:)方法,新segue的标识符和两个分支标识符(AddItem、ShowDetail)将都不匹配。这种情况,switch语句会在控制台打印错误信息,并终止应用。

你的 prepare(for:sender:)方法现在看起来是这样的:

        override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
            
            super.prepare(for: segue, sender: sender)
            
            switch(segue.identifier ?? "") {
                
            case "AddItem":
                os_log("Adding a new meal.", log: OSLog.default, type: .debug)
                
            case "ShowDetail":
                guard let mealDetailViewController = segue.destination as? MealViewController else {
                    fatalError("Unexpected destination: \(segue.destination)")
                }
                
                guard let selectedMealCell = sender as? MealTableViewCell else {
                    fatalError("Unexpected sender: \(sender)")
                }
                
                guard let indexPath = tableView.indexPath(for: selectedMealCell) else {
                    fatalError("The selected cell is not being displayed by the table")
                }
                
                let selectedMeal = meals[indexPath.row]
                mealDetailViewController.meal = selectedMeal
                
            default:
                fatalError("Unexpected Segue Identifier; \(segue.identifier)")
            }
        }

你现在已经有了逻辑实现,那么打开 MealViewController.swift,来确保UI正确的更新。具体来说,当一个MealViewController(菜品详情场景)的实例被被创建的时候,它的视图应该被菜品属性的数据所占据,如果这些数据存在的话。

你要在viewDidLoad()方法中做这一步的工作。

更新viewDidLoad()的实现

  1. 打开MealViewController.swift。
  2. 在MealViewController.swift.中,找到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.
            updateSaveButtonState()
        }
  1. 在nameTextField.delegate行下面,添加下面的代码。如果meal属性非空,这个代码会设置MealViewController中的每个视图,以便显示来自meal属性的数据。meal属性只有在非空的时候,现有的菜品才能被编辑。
        // Set up views if editing an existing Meal.
        if let meal = meal {
            navigationItem.title = meal.name
            nameTextField.text   = meal.name
            photoImageView.image = meal.photo
            ratingControl.rating = meal.rating
        }

你的viewDidLoad()方法看上去是这样的:

        override func viewDidLoad() {
            super.viewDidLoad()
            
            // Handle the text field’s user input through delegate callbacks.
            nameTextField.delegate = self
            
            // Set up views if editing an existing Meal.
            if let meal = meal {
                navigationItem.title = meal.name
                nameTextField.text = meal.name
                photoImageView.image = meal.photo
                ratingControl.rating = meal.rating
            }
            
            // Enable the Save button only if the text field has a valid Meal name.
            updateSaveButtonState()
        }

检查点:运行应用。从菜品列表点击菜品导航到菜品详情场景。这个详情场景应该使用相关菜品的数据进行了预填充。不幸的是,Save按钮还不能工作。如果点击Save,应用不会更新这个菜品,它只会添加一个新的菜品。接下来,你要修复这个问题。

image: ../Art/IEDB_sim_editmeal_2x.png

为了更新已存在的菜品,你要对unwindToMealList(sender:)方法进行修改,让它能够处理两种不同的情况:添加一个新菜品和修改一个已有的菜品。回顾一下,这个方法只是在用户点击Save按钮的时候调用,所以你不需要在这个方法中考虑Cancel按钮。

更新unwindToMealList(sender:)的实现方法以便既能添加又能编辑菜品

  1. 打开 MealTableViewController.swift。
  2. 在 MealTableViewController.swift中,找到unwindToMealList(sender:)方法。
        @IBAction func unwindToMealList(sender: UIStoryboardSegue) {
            if let sourceViewController = sender.source as? MealViewController, let meal = sourceViewController.meal {
                
                // Add a new meal.
                let newIndexPath = IndexPath(row: meals.count, section: 0)
                
                meals.append(meal)
                tableView.insertRows(at: [newIndexPath], with: .automatic)
            }
        }
  1. 在if语句开始的地方,添加下面的if语句:
        if let selectedIndexPath = tableView.indexPathForSelectedRow {
        }

这个代码检查在这个table view中是否有一个行杯选中。如果有,这就意味着用户点击了一个cell用以编辑菜品。换句话说,这个if语句会在你编辑已有菜品的时候被执行。

  1. 在这个if语句中,添加如下代码:
        // Update an existing meal.
        meals[selectedIndexPath.row] = meal
        tableView.reloadRows(at: [selectedIndexPath], with: .none)

第一行更新meals数组。它使用新的菜品对象替换了旧的。第二行冲加载了table view中的相应的行。这使用一个包含更新了菜品的数据的新cell代替了当前的cell,结果就是,当table view再次出现是,这个用户选择的行现在显示的是编辑过的菜品。

  1. 在if语句后面,添加一个else分支,并用花括号包裹这个方法中的最后四行。确保这些行都在else分支语句正确的缩紧,可以在选中这些行的情况下按下Control-I实现。
        else {
            // Add a new meal.
            let newIndexPath = IndexPath(row: meals.count, section: 0)
            
            meals.append(meal)
            tableView.insertRows(at: [newIndexPath], with: .automatic)
        }

这个else分支语句会在没有选中table view 的行时执行,这也就是说用户点击的是菜品详情场景中的加号按钮。换句话说,这个else分支语句是当用户添加新菜品的时候调用。

现在你的unwindToMealList(sender:)方法看上去是这样的。

        @IBAction func unwindToMealList(sender: UIStoryboardSegue) {
            if let sourceViewController = sender.source as? MealViewController, let meal = sourceViewController.meal {
                
                if let selectedIndexPath = tableView.indexPathForSelectedRow {
                    // Update an existing meal.
                    meals[selectedIndexPath.row] = meal
                    tableView.reloadRows(at: [selectedIndexPath], with: .none)
                }
                else {
                    // Add a new meal.
                    let newIndexPath = IndexPath(row: meals.count, section: 0)
                    
                    meals.append(meal)
                    tableView.insertRows(at: [newIndexPath], with: .automatic)
                }
            }
        }

检查点:运行应用。点击table view cell导航到菜品详情场景,并且看到它被菜品数据预填充。如果你点击Save,你所做的改变就会显示在菜品列表。

image: ../Art/IEDB_sim_overwritemeal_2x.png

取消编辑已有菜品

用户可能决定不保存已编辑的菜品,并且不做任何改变的返回到菜品列表。为此,你将更新Cancel按钮的行为来恰当的移除场景。

移除样式是基于场景被呈现的样式的。当用户点击Cancel按钮的时候,你将实现一个检查来确定当前场景是如何被呈现的。如果是模态方式呈现(用户点击加号按钮),它将使用dismissViewControllerAnimated(_:completion:)方法移除。如果它是使用push导航呈现的(用户点击table view cell),它将通过呈现它的导航控制器来移除它。

进一步探索
不同的呈现方式有不同的用处。当表达一个任务在继续之前用户必须完成或取消它时(例如添加一个新菜品),使用模态方式呈现场景。当用户要浏览分级数据时(例如,从菜品列表中选择一个菜品),使用导航控制器呈现场景。
更多信息,查看iOS Human Interface Guidelines.中的Interaction > Modality and Interaction > Navigation。

改变cancel方法的实现

  1. 打开MealViewController.swift。
  2. 在MealViewController.swift中,找到cancel(_:)方法。
        @IBAction func cancel(_ sender: UIBarButtonItem) {
            dismiss(animated: true, completion: nil)
        }

这个实现方法现在只使用 dismiss(animated:completion:)来移除菜品详情场景,因为迄今为止你只使用加号按钮。

  1. 在 cancel(_:)方法中,在已存在的代码之前,添加如下按钮:
        // Depending on style of presentation (modal or push presentation), this view controller needs to be dismissed in two different ways.
        let isPresentingInAddMealMode = presentingViewController is UINavigationController

这代码创建了一个布尔值,它表明呈现这个场景的视图控制器是否是UINavigationController类型。就像变量名isPresentingInAddMealMode表示的,菜品详情场景是通过用户点击加号按钮而被呈现的。这是因为在这中情况下菜品详情场景嵌套在它自己的导航控制器内,也就是导航控制器呈现了它。

  1. 在你刚添加的代码下面,添加下面的if语句,并把dismissViewControllerAnimated方法移到它里面:
        if isPresentingInAddMealMode {
            dismiss(animated: true, completion: nil)
        }

之前,当用户点击Cancel按钮的时候你总是调用dismiss(animated:completion:)方法;但是只有当用户添加新菜品的时候dismiss(animated:completion:)才会工作。因此,现在代码在调用dismiss(animated:completion:)之前先检查用户是否要添加一个新菜品。注意,当用户是在编辑菜品的时候,场景仍然不会移除。接下来你会添加该代码。

  1. 紧跟着if语句,添加else分支语句:
        else if let owningNavigationController = navigationController{
            owningNavigationController.popViewController(animated: true)
        }

如果用户正在编辑一个已存在的菜品,这个else块会被调用。这也意味着当用户选择菜品列表上的菜品的时候,菜品详情场景会被压栈道导航栈上。这个else语句使用一个if let语句来安全的解包视图控制器的navigationController属性。如果视图控制器已经被压栈到导航栈,这个属性会包含一个指向栈的导航控制器的引用。
在else分支语句中的代码执行一个名为popViewController(animated:)的方法,这个方法会把当前视图控制器(菜品详情场景)从导航栈中弹出,并进行动画转换。这会移除菜品详情场景,返回到菜品列表。

  1. 紧接着第一个else语句立刻添加第二个else分支语句:
        else {
            fatalError("The MealViewController is not inside a navigation controller.")
        }

这个else分支语句只有在菜品详情场景既不在模态当行控制器(例如添加一个新菜品)中也不在导航栈(例如编辑菜品)的时候才会被执行。如果你的应用导航流设置正确,这个else分支不会被执行。如果它执行,代表你的应用有错误。这个else分支会在控制台打印一个错误信息,并终止应用。

你的 cancel(_:)方法看上去是这样的:

        @IBAction func cancel(_ sender: UIBarButtonItem) {
            // Depending on style of presentation (modal or push presentation), this view controller needs to be dismissed in two different ways.
            let isPresentingInAddMealMode = presentingViewController is UINavigationController
            
            if isPresentingInAddMealMode {
                dismiss(animated: true, completion: nil)
            }
            else if let owningNavigationController = navigationController{
                owningNavigationController.popViewController(animated: true)
            }
            else {
                fatalError("The MealViewController is not inside a navigation controller.")
            }
        }

检查点:运行应用。当你选择菜品的时候,你能点击Cancel来返回到菜品列表而不需要保存任何改变。另外,当你点击加号按钮然后点击Cancel按钮的时候,会带你回到菜品列表而不添加新菜品。

支持删除菜品

接下来,你将给用户一个从菜品列表删除菜品的功能。你需要一个方式让用户把table view设置到编辑模式,从中它们可以删除cell。你可以通过添加一个Edit按钮到table view的导航栏来实现它。

添加Edit按钮到table view

  1. 打开MealTableViewController.swift。
  2. 在MealTableViewController.swift中,找到 viewDidLoad()方法。
        override func viewDidLoad() {
            super.viewDidLoad()
            
            // Load the sample data.
            loadSampleMeals()
        }
  1. 在super.viewDidLoad()下面,添加如下代码:
        // Use the edit button item provided by the table view controller.
        navigationItem.leftBarButtonItem = editButtonItem

这个代码创建了一个特定类型的bar button item,它内建了编辑行为。然后把它添加到菜品列表场景的导航栏的左侧。

你的viewDidLoad()方法看起来是这样的:

        override func viewDidLoad() {
            super.viewDidLoad()
            
            // Use the edit button item provided by the table view controller.
            navigationItem.leftBarButtonItem = editButtonItem
            
            // Load the sample data.
            loadSampleMeals()
        }

检查点:运行应用。注意有一个Edit按钮在table view的导航栏左侧。如果你点击这个按钮,table view就进入到了编辑模式——但是你还不能删除cell,因为你还没有实现它。

image: ../Art/IEDB_sim_editbutton_2x.png
image: ../Art/IEDB_sim_editbutton_2x.png

要想在table view上执行任何类型的编辑,你需要实现一个它的委托方法,tableView(_:commit:forRowAt:)。这个委托方法管理行在它处于编辑模式时的改变。

去除tableView(_:commit:forRowAt:)实现方法的注释。

删除菜品

  1. 在MealTableViewController.swift中,找到tableView(_:commit:forRowAt:)方法并删除它的注释。
    你的模版实现方法看上去是这样的:
        // Override to support editing the table view.
        override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
            if editingStyle == .delete {
                // Delete the row from the data source
                tableView.deleteRows(at: [indexPath], with: .fade)
            } else if editingStyle == .insert {
                // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
            }
        }
  1. 在// Delete the row from the data sourc注释下面,添加:
        meals.remove(at: indexPath.row)

这段代码从meals删除这个Meal对象。接下来的那行代码用来从table view删除行。

  1. 在 MealTableViewController.swift中,找到tableView(_:canEditRowAt:)并删除注释。
    删除之后,这个模版实现看上去是这样的:
        // Override to support conditional editing of the table view.
        override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
            // Return false if you do not want the specified item to be editable.
            return true
        }

你的tableView(_:commitEditingStyle:forRowAtIndexPath:)方法看上去是这样的:

        // Override to support editing the table view.
        override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
            if editingStyle == .delete {
                // Delete the row from the data source
                meals.remove(at: indexPath.row)
                tableView.deleteRows(at: [indexPath], with: .fade)
            } else if editingStyle == .insert {
                // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
            }
        }

检查点:运行应用。如果你点击Edit按钮,table view进入编辑模式。你通过左侧的指示器来选择要删除cell,并通过点击cell中的Delete按钮来确定删除。或者,向左滑动cell快速显示出Delete按钮;这是个table view内建的行为。当你点击Delete按钮的时候,cell从列表中被移除。

image: ../Art/IEDB_sim_deletebehavior_2x.png
image: ../Art/IEDB_sim_deletebehavior_2x.png

进一步探索
当在编辑模式的时候,评分控件扩展到了删除按钮中。这时因为cell的布局并没有使用Auto Layout的关系。这个控件适合正常分配的控件,但当控件减小时控件不能适应。
为了修好它,你需要使用嵌套的栈视图并使用Auto Layout的约束来布局;这是个留个读者的练习。
更多信息,参见Auto Layout Guide.

小结

在本课中,你为菜品列表添加编辑和删除的菜品的支持。因为编辑一个菜品和创建一个新菜品非常相似,所以应用使用了同一个菜品详情场景。结果是,在你呈现视图控制器的时候你需要区分呈现的方式,模态的还是压栈的。你要基于它的呈现方式来修改菜品详情场景的外观和行为。

你能添加、编辑、和删除菜品。但是这些数据没有保存。每次重启应用的时候,你总是从初始样本数据开始。在下一课中,你将添加代码来保存和加载菜品列表。

注意
想看本课的完整代码,下载这个文件并在Xcode中打开。
下载文件

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容