Storyboard使用教程二

在本系列Storyboard教程的第一部分,我们已经学习了如何使用Interface Builder创建并连接不同的视图控制器,还有如何直接在Storyboard编辑器中创建自定义表项。

本教程的第二部分,也是最终部分,内容包括segue(转场),static table view cell(静态表项),添加玩家页面和游戏选择页面!

好,现在让我们一起探索Storyboard的其他酷炫特性吧!

转场(Segue)

让我们向Storyboard中继续添加视图控制器,创建一个让用户添加新玩家的页面。

打开Main.storyboard,在包含表视图的那个Players场景的导航栏右侧拖入一个Bar Button Item(栏按钮项),在属性检查器中将Identifier设为Add,使其成为标准添加(加号)按钮。


当用户点按这个按钮时,你希望App会弹出一个模态页面让用户输入新玩家的详细信息。

在Players场景的右边拖入一个新的Navigation Controller(导航控制器)。记得双击面板可以缩放画面腾出空间。新加入的导航控制器附带一个表视图控制器,很方便。

这里有个小技巧:选择刚才在Players页面里加入的加号按钮,按住control键把它拖向新建的导航控制器,松手,在弹出的小选单中选择modal(模态)。

还记得吗:当Storyboard面板处于缩小状态时,无法添加或修改内容。如果在创建转场时遇到问题,请尝试双击放大!


现在Players页面和导航控制器之间多了一个新箭头。

这种连接的类型叫做segue(转场,读作seg-way,源自电影术语,原指两个场景间的过渡衔接),表示一个页面到另一个页面的过渡。此前我们所见的Storyboard连接描述的都是视图控制器的包含关系,而转场是用来切换页面的。转场可以由点击按钮、表项、手势等条件触发。

使用转场的好处是,再也不用为呈现新页面写代码了,也不用把按钮连接到IBAction方法上,你只需要在Storyboard中从一个栏按钮项拖到下一个页面就可以创建过渡了。(注:如果你的控件已经绑定了IBAction连接,该连接会被转场屏蔽。)

运行App,点击加号按钮,一个新的表视图会从屏幕下方滑入。


这就是所谓的模态转场。新页面完全覆盖原页面,在关闭模态页面之前,用户只能在新页面进行交互。后面我们还会看到push(入栈)转场,这种转场会把新页面压入导航控制器的导航栈(navigation stack)。

现在新页面还没什么用,连关闭页面返回都做不到,有去无回,因为转场是单向操作。

为返回页面,Storyboard提供了unwind(回退)转场。接下来我们要实现返回功能,主要分三个步骤:

  1. 创建让用户点选的控件,通常是个按钮。

  2. 在你想返回的控制器创建回退方法。

  3. 在Storyboard中将控件与回退方法连接。

首先打开Main.storyboard,选择新的表视图控制器场景(叫“Root View Controller”的那个)。双击导航栏,把标题改成“Add Player”。然后在导航栏添加两个栏按钮项,在属性检查器中设置左侧按钮的Identifier为Cancel,右侧按钮为Done,并将右侧按钮的Style改成Done。


接下来在项目中用Cocoa Touch Class模板添加一个新文件,命名为PlayerDetailsViewController并令其继承UITableViewController。要把这个类关联到Storyboard,先切回Main.storyboard,选择添加玩家的场景,然后在身份检查器(Identity inspector)中设Class为PlayerDetailsViewController。这个步骤我经常忘掉,在此特地提醒,还请读者牢记。

现在终于可以创建回退转场了。在PlayersViewController.swift(不是detail那个)的类定义下面添加如下的回退方法:

@IBAction func cancelToPlayersViewController(segue:UIStoryboardSegue) {
     dismissViewControllerAnimated(true, completion: nil)
}

@IBAction func savePlayerDetail(segue:UIStoryboardSegue) {
    dismissViewControllerAnimated(true, completion: nil)
}

这两个方法在调用时都会解除这个控制器。后面你会改写savePlayerDetail,让它名副其实地履行自己的职责。

最后回到Interface Builder,把Cancel按钮和Done按钮连接到相应的action方法上。按住control从栏按钮拖到视图控制器上面的出口(exit)对象上,然后从弹出的选单中选择正确的action名称。


记住取消方法的方法名,创建回退转场时,App中的所有回退方法(形如@IBAction func methodname(segue:UIStoryboardSegue))都会在列表中显示,所以命名方法时要多加注意,避免混淆。

运行App,点击加号按钮,然后测试Cancel和Done按钮。仅仅几行代码就可以实现如此功能。

静态表项(Static Cell)

完成这部分后,添加玩家页面会像这样:


当然这是一个分组表视图(grouped table view),但不必为该表创建数据源,也不必为此编写cellForRowAtIndexPath方法,你可以直接在Interface Builder中完成设计。这个特性叫做静态表项(static cell)。

选中Add Player场景的表视图,在属性检查器中设Content为Static Cells,把Style由Plain改成Grouped,并为表视图设置两个分段(section)。


修改Sections属性值时,编辑器会复制已有的分段。(你也可以在左侧的文档大纲中选择特定分段并复制。)

最终页面每个分段应该只有一行,请在面板或文档大纲中选中并删除多余的表项。

在文档大纲中选择最上面的表视图分段,在属性选择器中设Header字段值为Player Name。


向该分段内拖入一个新的Text Field(文本字段),横向拉长并移除边框,使文本字段控件融入周围环境。设字体为 System 17.0 ,勾掉Adjust to Fit选项。


接下来我们要用Xcode的Assistant Editor(辅助编辑器)功能为该文本字段在PlayerDetailsViewController中创建一个outlet。在Storyboard中,点击工具栏上的按钮(图标是两个套在一起的圆圈)打开辅助编辑器,应该会自动打开PlayerDetailsViewController.swift(如果没有,在右侧的跳转栏中选择相应文件)。

选择新建的文本字段,按住control拖到swift文件的类定义下面。在弹出框中将新outlet命名为nameTextField并点击Connect。在点击Connect后Xcode会在PlayersDetailViewController类中添加属性并在Storyboard中建立连接:


为表项上的视图创建outlet对于原型表项来说可能会遇到问题,这在上一部分的教程中提到过,不过静态表项就不必担心了,因为每个静态表项都只会有唯一的实例,把子视图与视图控制器的outlet连接完全没问题。

把第二分段的静态表项的Style设为Right Detail,这会套用一个标准表项样式,双击左侧的label,把文本改为Game,然后为该表项设定Disclosure Indicator(展开方向标)附件。


仿照刚才的Name文本字段,为右面的label("Detail"的那个)创建outlet并命名为detailLabel,该表项上的label都是常规UILabel对象。在建立连接前选择Detail文本字段时可能需要多次点击,请确保选择的是label而不是整个表项。完成后如图:


添加玩家页面的最终设计效果如图:


目前在Storyboard中设计的页面尺寸都符合iPhone 5的4英寸屏幕,高度为568点。当然你的App应当在不同的屏幕尺寸下正常工作,你可以在Storyboard中预览所有的尺寸。

在工具栏上点开辅助编辑器,选择跳转栏中的Preview。点击辅助编辑器左下角的加号添加新的预览尺寸,如果想删除一个屏幕尺寸,选中并按delete键即可。


一个简单的评分App不需要什么花哨的东西,只是使用表视图控制器,页面自动缩放以填满屏幕空间。当你想为不同的屏幕尺寸适配布局时,你需要使用Auto Layout和Size Classes。

构建并运行App,你会注意到添加玩家页面依然是空白!


表视图控制器在使用静态表项时不需要数据源,而之前你用Xcode模板创建的PlayerDetailsViewController类中依然有部分数据源相关代码,静态表项因此无法正常工作,所以静态内容没有显示出来。我们这就来解决问题!

打开PlayerDetailsViewController.swift文件,删除这一条代码往下的所有内容(注意不要删掉类自己的括号):

// MARK: - Table view data source

现在,自从加入这个类以后Xcode显示的那几条警告(warning)也应该消失了。

运行App,检查使用静态表项的新页面。完全没有写代码,其实刚才还删了一段代码!

还要了解一点:静态表项只在UITableViewController中有效,虽然Interface Builder允许你在常规UIViewController中的表视图对象里添加静态表项,运行时不会发挥作用,原因是UITableViewController中额外实现了一些用来处理静态表项数据源的操作。在项目中误用的话Xcode甚至会拒绝编译,输出报错信息:“Illegal Configuration: Static table views are only valid when embedded in UITableViewController instances”。

另一方面,原型表项在常规视图内的表视图中可以正常工作,但在nib中就没戏了。目前来讲,使用原型表项或静态表项就必须使用Storyboard。

你也有可能想在一个表视图中混合使用静态表项和常规的动态表项,很遗憾的是目前的SDK对此支持欠佳。如果你的App有这种需求,请参考苹果开发者官方论坛上的相关帖子寻求可行方案。

注:如果构建的页面上包含的静态表项多到无法在可视范围内全部展示,你可以在Interface Builder中直接利用滚动手势查看,这个功能可能不容易发现,但确实管用。

不过总的来说该写代码的地方只能靠代码,甚至静态表项的表视图也是如此。前面在把文本字段拖进第一个表项的时候,你可能发现尺寸不大合适,文本字段周围有一点白边,而且用户看不到文本字段的实际范围,如果正好点在边框上,没有弹出键盘,用户会感到困惑。

为避免这种情况,你应该让那一行任意位置接受的点击都可以唤出键盘。要这样做很容易,打开PlayerDetailsViewController.swift并如下添加

tableView(_:didSelectRowAtIndexPath:)`方法:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.section == 0 {
     nameTextField.becomeFirstResponder()
    }
}

代码的意思是如果用户点按第一个表项,App应该激活相应文本字段。该分段只有一个表项,你只需使用分段的索引。设文本字段为第一响应者会自动唤出键盘。这只是一小处用户体验优化,但就是这样一个小细节可以给用户省去一点烦恼。

小诀窍:添加delegate委托方法或重写视图控制器方法时,直接输入方法名开头的几个字母(前面不加func),即可在自动补全列表中选择正确的方法。

另外,还应该在Storyboard的属性检查器中把相应表项的Selection Style设为None(原本是Default),否则用户点按文本字段周围的边框时该行会高亮。


好啦,添加玩家页面设计完成。现在我们要实现功能。

为添加玩家页面实现功能

现在先不管Game这行,只输入玩家名称。

当用户点击Cancel按钮时,页面关闭,用户刚刚输入的数据随之作废。这部分功能直接用回退转场已经实现好了。

而当用户点击Done时,你应该创建一个新的Player对象,参照用户输入填充属性后更新玩家列表。

转场即将发生时,prepareForSegue(:sender:)会被调用。你可以重写这个方法,在退出视图之前将数据保存到一个新的Player对象中。

注:不要擅自调用prepareForSegue方法,这是UIKit通知你一个转场刚刚被触发的消息。

在PlayerDetailsViewController.swift中,先在类上添加一条属性:

var player:Player!

这条语句并不会将属性实例化,但其中的感叹号把该变量定义为隐式解包可选量(implicitly unwrapped optional),意思是该变量必须被实例化,而且你确定它在被使用前一定有值。

接下来在PlayerDetailsViewController.swift中添加以下方法:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "SavePlayerDetail" {
     player = Player(name: self.nameTextField.text, game: "Chess", rating: 1)
    }
}

prepareForSegue(_:sender:)方法判断转场的标识符是否为SavePlayerDetail,当且仅当判定结果为真时,创建一个新的Player实例,其中game和rating均取默认值。如果此时运行,App会崩溃,因为不存在标识符SavePlayerDetail`,player不会被实例化,结合前面的隐式解包可选量定义,引发运行时错误。

小提示:如果App出现诡异的崩溃问题,而且代码看起来似乎并无逻辑错误,那么可能是在代码中删除过对象或修改过对象名,以致Storyboard引用对象出错。

在Main.storyboard中,在文档大纲里找到Add Player场景,选择连接到savePlayerDetail这个action的回退转场,将其标识符改为SavePlayerDetail


然后选择连接到cancelToPlayersViewController的回退转场,将其标识符改为CancelPlayerDetail。以供prepareForSegue(_:sender:)方法判断标识符。

转到PlayersViewController类,如下修改回退转场方法savePlayerDetail(segue:)

@IBAction func savePlayerDetail(segue:UIStoryboardSegue) {
let playerDetailsViewController = segue.sourceViewController    as PlayerDetailsViewController

//add the new player to the players array
players.append(playerDetailsViewController.player)

//update the tableView
 let indexPath = NSIndexPath(forRow: players.count-1,   inSection: 0)
 tableView.insertRowsAtIndexPaths([indexPath],  withRowAnimation: .Automatic)

//hide the detail view controller
dismissViewControllerAnimated(true, completion: nil)
}

这会通过传入方法的转场引用获取一个指向PlayerDetailsViewController的引用,并借此向数据源中使用的Player数组添加新的Player对象,然后通知表视图在末尾新增了一行,因为表视图和数据源应当保持同步。

你可能会直接调用tableView.reloadData(),但还是为新行插入的操作加入动画效果比较好。UITableViewRowAnimation.Automatic会以插入新行的位置自动选用合适的动画,十分方便。

试试看,现在应该可以向列表中加入新玩家了!

性能

现在Storyboard已经有好几个视图控制器了,你或许会担心性能问题,不过一次载入整个Storyboard并不是什么苦活,Storyboard不会立即实例化所有的视图控制器,立即载入的只有初始视图控制器。而由于这里的初始视图控制器是一个分页栏控制器,包含的两个视图控制器也会被载入(第一个分页标签的Players场景和第二个分页标签的场景)。

其他视图控制器只有在转场过去的时候才会被实例化。而当关闭视图控制器的时候,它们会立即被释放,所以内存中只有活跃使用的视图控制器,就好像分别使用nib一样。

实践是检验真理的唯一标准,在PlayerDetailsViewController类中添加构造器(initializer)和析构器(deinitializer):

required init(coder aDecoder: NSCoder) {
    println("init PlayerDetailsViewController")
    super.init(coder: aDecoder)
}

deinit {
     println("deinit PlayerDetailsViewController")
}

你刚刚重写了init(coder:)deinit方法,让它们向Xcode调试面板输出信息。现在运行App,打开添加玩家页面,你会发现视图控制器只有在被打开的时候才会分配。

关闭添加玩家页面的时候,无论是点击Cancel还是Done都会看到deinit析构器的println()输出。如果再次打开这个页面,你还会看到init(coder:)的输出,这样你应该相信这个事实了:视图控制器是按需加载的,就像手动载入nib一样。

注:如果你以前用过nib,那么你应该会很熟悉构造器init(coder:),这部分机制延续到了Storyboard中:使用的方法依然是init(coder:)awakeFromNib()viewDidLoad()。Storyboard可以看成附带了过渡信息和关联信息的一系列nib的集合,而Storyboard内的视图和视图控制器使用与nib相同的方式编码并解析。

游戏选择页面

在添加玩家页面中点选Game行应该打开一个新页面并让用户从列表中选择一个游戏,这意味着下一步要加入另外一个表视图控制器,不过这次的页面不是模态显示,而是压入导航栈。

向Storyboard中拖入一个新的表视图控制器,在添加玩家页面中选择Game表项(确保选中的是整个表项,而不是其中的label),然后按住control拖到新建的表视图控制器,在两者之间创建转场。在弹出的选单中选择转场类型为Push,然后在属性检查器中把转场的Identifier标识符设为PickGame。

双击导航栏,将新场景命名为Choose Game。设原型表项的Style为Basic(基本),设重用标识符为GameCell,如图:

1421214153697020.png

在项目中使用Cocoa Touch Class模板新建一个Swift文件,命名为GamePickerViewController,继承UITableViewController。回到Storyboard中将游戏选择页面的Custom Class设为GamePickerViewController

现在为新页面添加数据。在GamePickerViewController.swift中,在开头添加games属性,然后重写viewDidLoad函数,像这样:

var games:[String]!

override func viewDidLoad() {
    super.viewDidLoad()
    games = ["Angry Birds",
       "Chess",
       "Russian Roulette",
       "Spin the Bottle",
       "Texas Hold'em Poker",
       "Tic-Tac-Toe"]
}

你刚刚新增了一个叫做games的字符串数组,并在viewDidLoad()中用写定的内容填充数组。

然后如下替换数据源方法:

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
     return 1
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
     return games.count
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
     let cell = tableView.dequeueReusableCellWithIdentifier("GameCell", forIndexPath: indexPath) as UITableViewCell
    cell.textLabel?.text = games[indexPath.row]
    return cell
}

上述代码将games数组设为数据源并替换表项的textLabel中的字符串值。

只要数据源准备就绪就应该能正常工作。运行App,点选Game行,新的游戏选择页面会滑入屏幕。现在点击各项不会有什么效果,但由于该页面呈现在导航栈上,你可以直接点击返回按钮,返回原来的添加玩家页面。


不用写代码就可以唤出新页面,是不是很赞?只要按住control从静态表项拖到新场景,写的代码只有填充表视图的内容,而且一般来讲比原地设计好的列表要灵活些(因为games数组更方便修改)。

当然新页面要返回数据才有用,为此你要添加一个新的回退转场。

在GamePickerViewController类的上面添加持有选中的游戏的名称和索引的属性:

var selectedGame:String? = nil
var selectedGameIndex:Int? = nil

然后修改cellForRowAtIndexPath:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("GameCell", forIndexPath: indexPath) as UITableViewCell
     cell.textLabel?.text = games[indexPath.row]

     if indexPath.row == selectedGameIndex {
        cell.accessoryType = .Checkmark
    } else {
     cell.accessoryType = .None
    }
    return cell
}

这会在当前所选游戏对应的表项附上选中标记(对号),这对用户体验来说不可或缺。

接着添加tableview(tableview:didSelectRowAtIndexPath:)方法:

    override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
     tableView.deselectRowAtIndexPath(indexPath, animated: true)

    //Other row is selected - need to deselect it
    if let index = selectedGameIndex {
        let cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: index, inSection: 0))
     cell?.accessoryType = .None
}

    selectedGameIndex = indexPath.row
    selectedGame = games[indexPath.row]

    //update the checkmark for the current row
    let cell = tableView.cellForRowAtIndexPath(indexPath)
    cell?.accessoryType = .Checkmark
}

这段代码首先会取消选择刚刚点选的行,外观会从灰色高亮变回常规的白色,然后移除对号,并在刚刚点选的行上附加选中标记。

运行App,测试是否正常。点选一个游戏名,相应行会附上选中标记,点选另一个游戏名,选中标记也随之移动。


按要求来说点选某行之后应该关闭该页面,不过现在并没有自动返回,因为尚未绑定回退转场。

在PlayerDetailsViewController.swift的类上面添加一个持有被选游戏的属性,以便之后在Player对象中保存。令其默认值为"Chess",这样一来新玩家总会有一个选定的游戏。

var game:String = "Chess"
同样在该文件中改写viewDidLoad()以在静态表项中游戏名称:

override func viewDidLoad() {
super.viewDidLoad()
detailLabel.text = game
}
添加回退转场方法:

@IBAction func selectedGame(segue:UIStoryboardSegue) {
    let gamePickerViewController = segue.sourceViewController as GamePickerViewController
    if let selectedGame = gamePickerViewController.selectedGame {
     detailLabel.text = selectedGame
     game = selectedGame
 }
     self.navigationController?.popViewControllerAnimated(true)
}

上述代码会在用户从选择游戏场景选中一个游戏后执行。该方法按照选中的游戏更新页面上的label和game属性,然后将GamePickerViewController弹出导航栈。

在Main.storyboard中按住control从表项拖到Exit出口对象,然后从弹出列表中选择selectedGame:


设该回退转场标识符为SaveSelectedGame。

运行App试试看,创建新玩家,点选Game行并选择一个游戏。


不幸的是,这个回退转场方法是在tableView(_:didSelectRowAtIndexPath:)方法前执行的,所以selectedGameIndex并未及时更新。幸运的是你可以重写prepareForSegue(_:sender:)方法,在转场之前完成更新操作。

在GamePickerViewController中添加prepareForSegue(segue:)方法:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == "SaveSelectedGame" {
        let cell = sender as UITableViewCell
        let indexPath = tableView.indexPathForCell(cell)
        selectedGameIndex = indexPath?.row
        if let index = selectedGameIndex {
        selectedGame = games[index]
    }
 }
}

prepareForSegue(_:sender:)的sender参数是引发转场的对象,在这里对应选中的游戏表项,所以你可以利用表项的indexPath来在games数组中确定选中的游戏并在转场发生之前更新selectedGame

现在运行App,选择游戏后玩家的游戏信息会随之更新了。


接下来改写PlayerDetailsViewController的prepareForSegue方法来返回选中的游戏,而不是写定的"Chess"。这样一来,完成添加玩家的操作后,Players场景中会显示玩家实际选择的游戏。

在PlayerDetailsViewController.swift中如下改写prepareForSegue(_:sender:)方法:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == "SavePlayerDetail" {
     player = Player(name: nameTextField.text, game:game, rating: 1)
 }
}

完成添加玩家页面并点击Done后,玩家列表会更新正确的游戏信息。

还有一点,当你选择一个游戏,返回添加玩家页面,然后尝试重新选择游戏的时候,之前选定的游戏应该显示选中标记。解决方法是在转场时把PlayerDetailsViewController中保存的选中的游戏传给GamePickerViewController。

还是在PlayerDetailsViewController.swift中,于prepareForSegue(segue:,sender:)方法的末尾添加以下代码:

if segue.identifier == "PickGame" {
    let gamePickerViewController = segue.destinationViewController as GamePickerViewController
     gamePickerViewController.selectedGame = game
}

注意:现在有两条检查segue.identifierif语句。SavePlayerDetail是返回玩家列表的回退转场,PickGame是前往游戏选择页面的入栈转场。添加的代码会在GamePickerViewController的视图加载之前更新其中的selectedGame

打开GamePickerViewController.swift并在viewDidLoad()末尾添加以下代码:

if let game = selectedGame {
     selectedGameIndex = find(games, game)!
}

这两行代码获取从PlayerDetailsViewController传进的selectedGame并将其转换成正确的索引。find()函数会在games数组中查找匹配selectedGame的String,然后返回匹配元素的索引,赋值给selectedGameIndex,这个索引用来在对应表项上设置选中标记。

好。现在选择游戏页面功能实现完成!

何去何从?

可喜可贺,现在你已经了解Storyboard编辑器的基本用法,能够创建包含多个视图控制器并能通过转场在场景之间切换的App!在一处集中管理多个视图控制器和互相的关联,让整体把握App的样子更加容易。

你也看到了自定义表视图和表项有多么容易。有了静态表项,不用实现所有的数据源方法也可以构建一些界面。

原文地址

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

推荐阅读更多精彩内容

  • Storyboard是最先在iOS 5引入的一项振奋人心的特性,大幅缩减构建App用户界面所需的时间。 要介绍St...
    余一波_Bobby阅读 60,680评论 11 76
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,614评论 4 59
  • 前言的前言 唐巧前辈在微信公众号「iOSDevTips」以及其博客上推送了我的文章后,我的 Github 各项指标...
    VincentHK阅读 5,241评论 4 44
  • 很多妈妈说没时间,要带孩子做家务。有的妈妈说,白天上班晚上带孩子,没时间。貌似大家确实很忙。 有个现象不知道大家发...
    双胞胎妈妈_9a17阅读 147评论 0 0
  • 当我不开心、郁闷、失意或失败时 我只是想朝我最亲近的你发发牢骚 希望听几句安慰的话语 而不是你喋喋不休的教育 或者...
    简晨332阅读 122评论 0 0