SnapKit框架解析(一) —— SnapKit应用的一个简单示例(一)

版本记录

版本号 时间
V1.0 2019.07.02 星期二

前言

OC中自动布局可以使用三方比如说Masonry,那么Swift呢?Swift也有很优秀的第三方框架,其中SnapKit就很优秀,接下来几篇我们就一起来看下这个框架。

开始

首先看下写作环境

Swift 5, iOS 12, Xcode 10

在本教程中,您将了解SnapKit,一种轻量级DSL(domain-specific language),使自动布局和约束变得轻而易举。

Auto Layout是一种功能强大的工具,用于描述应用程序中不同视图和复杂视图层次结构之间的关系和约束,但是编写这些约束通常在开始时非常不直观。

直到几年前,以编程方式编写这些约束对于使用Visual Formatting Language或手动创建NSLayoutConstraints等神秘和冗长的方法非常繁琐。

iOS 9通过引入布局锚点(Layout Anchors)大大改进了这些机制,这使得创建约束非常直观。 然而,仍然有很多东西需要让你创建约束更加快捷。 这正是SnapKit发挥作用的地方!

在本教程中,您将使用SnappyQuiz - 一个简单的游戏,玩家可以获得随机问题/陈述,并选择是真还是假。

打开SnappyQuiz.xcworkspace而不是项目文件 - 这很重要。

该项目包含一些您需要的Swift文件:

  • QuizViewController.swift:这是屏幕布局发生的地方,包括定义视图。
  • QuizViewController + Logic.swift:这个文件包含游戏本身的逻辑。您不需要在本教程中更改此文件。
  • QuizViewController + Constraints.swift:屏幕UI的所有约束都位于此文件中,您可以在此处完成大部分工作。

该项目还包括表示游戏状态的State.swift和找到原始问题数据的Questions.swift,但在本教程中您不会真正触及这些。

构建并运行项目。您应该看到倒数计时器滴答的第一个问题以及代表当前游戏进度的进度条:

QuizViewController + Constraints.swift中,探索setupConstraints()。此代码使用上述布局锚点来定义应用程序中不同视图之间的关系。

在本教程中,您将使用其SnapKit变体替换每个约束。


Snappin’ & Chainin’

在您实际修改SnappyQuiz应用程序之前,您应该更多地了解SnapKit的实际内容。在本教程的介绍中,我提到SnapKit使用DSL,但这究竟意味着什么?

1. What is a DSL?

域特定语言domain-specific language(DSL)是为表达和处理特定域或解决特定问题而创建的语言。

SnapKit的案例中,它旨在创建一种更直观,易于使用的语法,专门用于自动布局(Auto Layout)约束。

需要了解的一件重要事情是,作为DSLSnapKit主要是syntactic sugar - 你可以在没有SnapKit的情况下做任何SnapKit。但是,SnapKit提供了更流畅和富有表现力的语法来解决这个特定的领域和问题。

2. SnapKit Basics

采用一组非常常见的约束 - 将视图附加到其所有superview的边缘:

没有SnapKit,代码看起来类似于以下内容:

child.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
  child.leadingAnchor.constraint(equalTo: parent.leadingAnchor),
  child.topAnchor.constraint(equalTo: parent.topAnchor),
  child.trailingAnchor.constraint(equalTo: parent.trailingAnchor),
  child.bottomAnchor.constraint(equalTo: parent.bottomAnchor),
])

这非常有声明性,但SnapKit可以做得更好。

SnapKit在系统中的每个UIView(和NSView,在macOS上)引入了一个名为snp的命名空间。 该命名空间以及makeConstraints(_ :)方法是SnapKit的精髓。

SnapKit表示这样的约束:

child.snp.makeConstraints { make in
  make.leading.equalToSuperview()
  make.top.equalToSuperview()
  make.trailing.equalToSuperview()
  make.bottom.equalToSuperview()
}

这可能看起来像是相似数量的代码,但它极大地提高了可读性。 你可能注意到的两件事是:

  • 1) 由于SnapKitequalToSuperview(),您根本不需要引用父项。 这意味着,即使子项移动到不同的父视图,您也不需要修改此代码。
  • 2) make语法创建了几乎与英语类似的语法,例如 “make leading equal to superview“,这是更好阅读。

3. Composability & Chaining

你刚刚看到了你的第一个SnapKit代码,但是SnapKit真正发挥作用的是它的组合功能。 您可以将任何锚点链接在一起,以及约束本身。

您可以将上面的示例重写为:

child.snp.makeConstraints { make in
  make.leading.top.trailing.bottom.equalToSuperview()
}

或者更简洁

child.snp.makeConstraints { make in
  make.edges.equalToSuperview()
}

想在视图中添加16inset? 另一个简单的链接将帮助你实现:

child.snp.makeConstraints { make in
  make.edges.equalToSuperview().inset(16)
}

正如您所看到的,可组合性和链接(composability and chaining)SnapKit的核心,并提供了使用vanilla NSLayoutConstraints无法实现的表现力。


Your First Constraints

现在您已经掌握了SnapKit的一些基础知识,现在是时候转换setupConstraints()中的所有约束来使用它了。 它比你想象的要简单得多,你将逐一探讨SnapKit的各种功能。

返回QuizViewController + Constraints.swift并找到setupConstraints()。 您将开始修改updateProgress(to:0)行以下的约束。 稍后您将回到该行之上的约束。

找到以下代码块,定义计时器label的约束:

lblTimer.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
  lblTimer.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.45),
  lblTimer.heightAnchor.constraint(equalToConstant: 45),
  lblTimer.topAnchor.constraint(equalTo: viewProgress.bottomAnchor, constant: 32),
  lblTimer.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])

替换成下面内容

lblTimer.snp.makeConstraints { make in
  make.width.equalToSuperview().multipliedBy(0.45) // 1
  make.height.equalTo(45) // 2
  make.top.equalTo(viewProgress.snp.bottom).offset(32) // 3
  make.centerX.equalToSuperview() // 4
}

和以前一样,这是使用SnapKit的链接语法直接翻译原始约束。 快速进行分解下:

  • 1) 使标签的宽度等于父视图的高度,乘以0.45(即超视图宽度的45%)。
  • 2) 将标签的高度设置为静态45。
  • 3) 将标签的顶部限制在进度条的底部,偏移32。
  • 4) 将X轴居中到superview的X轴,使标签水平居中。

虽然与基于NSLayoutConstraint的代码没有太大区别,但它提供了更好的可读性和受约束的视图的范围。

注意:注意其他不同的东西? SnapKit不再要求您将translatesAutoresizingMaskIntoConstraints设置为false! 库为你做好。 不再忘记这样做并且不知疲倦地调试混乱的约束。

1. Do That Again

转到下一个UI元素 - 问题label。 找到以下代码:

lblQuestion.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
  lblQuestion.topAnchor.constraint(equalTo: lblTimer.bottomAnchor, constant: 24),
  lblQuestion.leadingAnchor
    .constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
  lblQuestion.trailingAnchor
    .constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16)
])

这里有三个限制。 在这一点上,逐一替换这些可能会让人感到熟悉。 第一个约束可以很容易地转换为:

make.top.equalTo(lblTimer.snp.bottom).offset(24)

最后两个约束也可以用同样的直接方式转化:

make.leading.equalToSuperview().offset(16)
make.trailing.equalToSuperview().offset(-16)

但实际上,您是否注意到这两个约束对leading锚点和trailing锚点执行相同的操作? 听起来像一个完美适合一些链接! 使用以下内容替换上面的整个代码块:

lblQuestion.snp.makeConstraints { make in
  make.top.equalTo(lblTimer.snp.bottom).offset(24)
  make.leading.trailing.equalTo(view.safeAreaLayoutGuide).inset(16)
}

注意两件事:

  • 1) 与前面的示例一样,leadingtrailing是链接的。
  • 2) 您不必总是使用snp来约束视图! 注意,这次,你的代码只是为一个好的UILayoutGuide创建一个约束。

另一个有趣的事实是inset选项不必是数字。 它也可以采用UIEdgeInsets结构。 您可以将上面的行重写为:

make.leading.trailing.equalTo(view.safeAreaLayoutGuide)
  .inset(UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16))

这在这里可能不太有用,但是当insets边缘不同时,它会变得非常有用。

两个限制下来,三个去!

2. A Quick Challenge!

下一个约束是您之前已经看到的约束 - 消息label的边缘应该简单地等于父视图的边缘。 你为什么不亲自尝试这个?

如果您遇到困难,请随时点击下面的按钮查看代码:

替换下面的内容

lblMessage.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
  lblMessage.topAnchor.constraint(equalTo: navView.topAnchor),
  lblMessage.bottomAnchor.constraint(equalTo: navView.bottomAnchor),
  lblMessage.leadingAnchor.constraint(equalTo: navView.leadingAnchor),
  lblMessage.trailingAnchor.constraint(equalTo: navView.trailingAnchor)
])

lblMessage.snp.makeConstraints { make in
  make.edges.equalToSuperview()
}

3. Final Constraint

移动到SnapKit的语法仍有一个最终约束。 水平UIStackView持有TrueFalse按钮。

找到以下代码:

svButtons.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
  svButtons.leadingAnchor.constraint(equalTo: lblQuestion.leadingAnchor),
  svButtons.trailingAnchor.constraint(equalTo: lblQuestion.trailingAnchor),
  svButtons.topAnchor.constraint(equalTo: lblQuestion.bottomAnchor, constant: 16),
  svButtons.heightAnchor.constraint(equalToConstant: 80)
])

像以前一样,leading and trailing约束可以链接,因为它们负责相同的关系。 但由于您不想为superview创建约束,这应该是什么样的?

用以下内容替换上面的代码:

svButtons.snp.makeConstraints { make in
  make.leading.trailing.equalTo(lblQuestion)
  make.top.equalTo(lblQuestion.snp.bottom).offset(16)
  make.height.equalTo(80)
}

注意makeConstraints闭包中的第一行 - 您只需定义前导和尾随约束应该等于lblQuestion! 不需要特殊性! SnapKit能够推断出你指的是lblQuestion的那些特定约束。

对于更简单的约束也是如此。 以下代码:

view.snp.makeConstraints { make in
  make.width.equalTo(otherView.snp.width)
  make.centerX.equalTo(otherView.snp.centerX)
}

可以被重写为

view.snp.makeConstraints { make in
  make.width.equalTo(otherView)
  make.centerX.equalTo(otherView)
}

请注意,不需要otherView的特性 - SnapKit根据关系中的第一个视图知道需要创建哪种约束。

您甚至可以通过简单编写来进一步减小代码大小:

view.snp.makeConstraints { make in
  make.width.centerX.equalTo(otherView)
}

哇! 多么酷啊?

构建并运行项目。 你会注意到它仍然像以前一样工作。很好~


Modifying Constraints

在本教程的前几节中,您学习了如何创建新约束。但是,有时您想要修改现有约束。

是时候尝试一些您可能想要这样做的用例,以及如何在SnapKit中实现这一点。

1. Updating a Constraint’s Constant

一些SnappyQuiz的用户对切换到横向时应用程序的外观非常沮丧。

当应用程序切换方向时,您可以通过修改UI的某些方面来使其更好,这样您就可以做到这一点。

对于此任务,您将以横向方向增加倒数计时器的高度,并增加字体大小。在此特定上下文中,您需要更新计时器标签的高度约束的常量。

当你只对更新常量感兴趣时,SnapKit有一个名为updateConstraints(_ :)的超级有用的方法,它在这里非常适合。

回到QuizViewController + Constraints.swift,在文件末尾添加以下代码:

// MARK: - Orientation Transition Handling
extension QuizViewController {
  override func willTransition(
    to newCollection: UITraitCollection,
    with coordinator: UIViewControllerTransitionCoordinator
    ) {
    super.willTransition(to: newCollection, with: coordinator)
    // 1
    let isPortrait = UIDevice.current.orientation.isPortrait
    
    // 2
    lblTimer.snp.updateConstraints { make in
      make.height.equalTo(isPortrait ? 45 : 65)
    }
    
    // 3
    lblTimer.font = UIFont.systemFont(ofSize: isPortrait ? 20 : 32, weight: .light)
  }
}

这增加了一个扩展,它将处理视图控制器的旋转。 这是代码的作用:

  • 1) 确定设备的当前方向
  • 2) 使用updateConstraints(_ :)并将计时器标签的高度更新为45(如果它是纵向) - 否则,将其设置为65。
  • 3) 最后,根据方向相应地增加字体大小。

以为会很难吗? 抱歉让你失望!

构建并运行项目。 应用程序在模拟器中启动后,按Command-Right ArrowCommand-Left Arrow更改设备方向。 请注意label如何根据设备的方向增加其高度和字体大小。

2. Remaking Constraints

有时,您需要的不仅仅是修改一些常量。 您可能希望完全更改特定视图上的整个约束集。 对于那种非常常见的情况,SnapKit有另一个有用的方法 - 你猜对了--rekekeConstraints(_ :)

SnappyQuiz中有一个完美的地方可以尝试这种方法:顶部的进度条。 现在,进度条的宽度约束保存在QuizViewController.swift中名为progressConstraints的变量中。 然后,updateProgress(to :)简单地销毁旧约束并创建一个新约束。

是时候看看你是否可以让这个烂摊子好一点。

回到QuizViewController + Constraints.swift,看看updateProgress(to:)。 它检查是否已存在约束,如果是,则将其停用。 然后,它创建一个新约束并激活它。

用以下内容替换updateProgress(to :)

func updateProgress(to progress: Double) {
  viewProgress.snp.remakeConstraints { make in
    make.top.equalTo(view.safeAreaLayoutGuide)
    make.width.equalToSuperview().multipliedBy(progress)
    make.height.equalTo(32)
    make.leading.equalToSuperview()
  }
}

哇,这更好! 整个有点神秘的代码片段完全被几行代码所取代。 remakeConstraints(_ :)只是每次都替换整个约束集,因此您不必手动引用约束并对其进行管理。

另一个好处是你可以进一步清理当前代码中的一些混乱。

setupConstraints()中,删除以下代码:

guard let navView = navigationController?.view else { return }

viewProgress.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
  viewProgress.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
  viewProgress.heightAnchor.constraint(equalToConstant: 32),
  viewProgress.leadingAnchor.constraint(equalTo: view.leadingAnchor)
])

该方法的第一行现在应该只是updateProgress(to:0)

最后,您可以在QuizViewController.swift中删除以下行:

/// Progress bar constraint
var progressConstraint: NSLayoutConstraint!

全部完成! 构建并运行您的应用程序,一切都应该像以前一样工作,但具有更清晰的约束管理代码。

3. Keeping a Reference

虽然你不会在SnappyQuiz中试验这个选项,但它仍然是你应该知道的。

在标准的NSLayoutConstraint方式中,您可以存储对约束的引用并在以后修改它。 SnapKit也可以使用Constraint类型:

var topConstraint: Constraint?

lblTimer.snp.makeConstraints { make in 
  // Store your constraint
  self.topConstraint = make.top.equalToSuperview().inset(16)
  make.leading.trailing.bottom.equalToSuperView()
}

// Which you can later modify
self.topConstraint?.update(inset: 32)

// Or entirely deactivate
self.topConstraint?.deactivate()

When Things Go Wrong

有时在生活中,事情出了问题。 在谈论自动布局(Auto Layout)约束时,情况更是如此。

回到QuizViewController+Constraints.swift,找到以下行:

make.centerX.equalToSuperview()

在它下面,但仍然在makeConstraints闭包内,添加:

make.centerY.equalToSuperview()

构建并运行应用程序。 正如您所看到的,UI完全被破坏了:

此外,正如预期的那样,您将在调试控制台中看到一个破坏约束的巨大墙,其外观应类似于以下内容:

[LayoutConstraints] Unable to simultaneously satisfy constraints.

"<SnapKit.LayoutConstraint:0x600001b251a0@QuizViewController+Constraints.swift#62 UIView:0x7f9371e004a0.top == UILayoutGuide:0x60000062c0e0.top>",
    "<SnapKit.LayoutConstraint:0x600001b25260@QuizViewController+Constraints.swift#64 UIView:0x7f9371e004a0.height == 32.0>",
    "<SnapKit.LayoutConstraint:0x600001b2dc80@QuizViewController+Constraints.swift#38 UILabel:0x7f9371e088c0.height == 45.0>",
    "<SnapKit.LayoutConstraint:0x600001b2dce0@QuizViewController+Constraints.swift#39 UILabel:0x7f9371e088c0.top == UIView:0x7f9371e004a0.bottom + 32.0>",
    "<SnapKit.LayoutConstraint:0x600001b2dda0@QuizViewController+Constraints.swift#41 UILabel:0x7f9371e088c0.centerY == UIView:0x7f9371e09a50.centerY>",
    "<NSLayoutConstraint:0x600001c6c2d0 'UIView-Encapsulated-Layout-Height' UIView:0x7f9371e09a50.height == 551   (active)>",
    "<NSLayoutConstraint:0x600001c61450 'UIViewSafeAreaLayoutGuide-top' V:|-(0)-[UILayoutGuide:0x60000062c0e0'UIViewSafeAreaLayoutGuide']   (active, names: '|':UIView:0x7f9371e09a50 )>"

Will attempt to recover by breaking constraint 
<SnapKit.LayoutConstraint:0x600001b2dc80@QuizViewController+Constraints.swift#38 UILabel:0x7fc53e41d060.height == 45.0>

好家伙。 你甚至从哪里开始? 所有你看到的是一堆内存地址,并不一定意味着太多。 理解哪些约束被打破也很困难。

幸运的是,SnapKit提供了一个很好的附加修饰符来跟踪这些问题,称为labeled(_:)

用以下内容替换整个lblTimer约束块:

lblTimer.snp.makeConstraints { make in
  make.width.equalToSuperview().multipliedBy(0.45).labeled("timerWidth")
  make.height.equalTo(45).labeled("timerHeight")
  make.top.equalTo(viewProgress.snp.bottom).offset(32).labeled("timerTop")
  make.centerX.equalToSuperview().labeled("timerCenterX")
  make.centerY.equalToSuperview().labeled("timerCenterY")
}

注意到每个约束的labeled(_:)添加? 这允许您为每个约束附加描述性标题,因此您不必选择内存地址并失去理智。

最后一次构建并运行您的应用程序。 你破坏的约束在这一点上应该提供更清晰的信息:

[LayoutConstraints] Unable to simultaneously satisfy constraints.

"<SnapKit.LayoutConstraint:0x60000365c4e0@QuizViewController+Constraints.swift#62 UIView:0x7fc53e4181d0.top == UILayoutGuide:0x600002b0ae60.top>",
"<SnapKit.LayoutConstraint:0x60000365e8e0@QuizViewController+Constraints.swift#64 UIView:0x7fc53e4181d0.height == 32.0>",
"<SnapKit.LayoutConstraint:timerCenterY@QuizViewController+Constraints.swift#41 UILabel:0x7fc53e41d060.centerY == UIView:0x7fc4fe507170.centerY>",
"<SnapKit.LayoutConstraint:timerHeight@QuizViewController+Constraints.swift#38 UILabel:0x7fc53e41d060.height == 45.0>",
"<SnapKit.LayoutConstraint:timerTop@QuizViewController+Constraints.swift#39 UILabel:0x7fc53e41d060.top == UIView:0x7fc53e4181d0.bottom + 32.0>",
"<NSLayoutConstraint:0x6000031346e0 'UIView-Encapsulated-Layout-Height' UIView:0x7fc4fe507170.height == 551   (active)>",
"<NSLayoutConstraint:0x600003139c70 'UIViewSafeAreaLayoutGuide-top' V:|-(0)-[UILayoutGuide:0x600002b0ae60'UIViewSafeAreaLayoutGuide']   (active, names: '|':UIView:0x7fc4fe507170 )>"

Will attempt to recover by breaking constraint 
<SnapKit.LayoutConstraint:timerHeight@QuizViewController+Constraints.swift#38 UILabel:0x7fc53e41d060.height == 45.0>

这看起来很相似,但要仔细看。 你可以看到像timerCenterY这样的宝石。 这提供了更多信息,并且您有一些很好的标记限制来开始调试。

更具体地说,您可以在此输出中识别的唯一三个标签是timerCenterYtimerHeighttimerTop。 由于高度是静态的,您可以确定冲突是在两个约束之间留下的。 这比缩小自动布局调试输出的原始混乱要快得多!

完成后,随意删除开始此混乱的centerY约束。

恭喜! 您现在已经了解了SnapKit所提供的大部分内容,但您仍需要考虑一些功能和修饰符,例如priority, divided等等。 查看SnapKit的官方GitHub仓库SnapKit’s official GitHub repo了解更多信息。

请记住,SnapKit可以帮助您创建一个易于使用,特定于问题的语法来创建约束,但它不提供常规NSLayoutConstraints无法实现的功能。 随意尝试两者并找到适合每种情况的良好中间立场。

后记

本篇主要讲述了SnapKit自动布局的框架,感兴趣的给个赞或者关注~~~

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

推荐阅读更多精彩内容