【总结回顾】iOS Apprentice Tutorial 2:Checklists(七)

这是***【总结回顾】iOS Apprentice Tutorial 2:Checklists ***系列的第七篇文章,也是最后一篇文章了,前几篇文章请见(一)(二)(三)(四)(五)(六)

本篇文章总结本书的第章( Improving the user experienceExtra feature: local notifications)中的重点内容,主要是讲述如何实现一些提升用户体验的功能,例如记录每个清单里未完成项目的数量和清单中项目总数,每次添加新的清单后能够给清单自动排序,可给清单增加小图标,让界面更好看,以及适配所有的机型。从205页到269页(最后一页)。

67. 优化用户体验之显示未完成数量

  func countUncheckedItems() -> Int {
    var count = 0
    for item in items where !item.checked {
      count += 1
    }
    return count
  }

此方法返回值就是此清单中没有完成的数量。当然此方法也可以有另外一种写法:

func countUncheckedItems() -> Int {
    var count = 0
    for item in items  {
      if !item.checked {
            count += 1
        }
    }
    return count
  }

然后在AllListsViewController.swift中加入下列方法,保持显示数据的同步:

  override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    tableView.reloadData()
  }

当然接下来还可以实现一些小功能,比如全部完成的时候文案显示全部完成,清单里还没有添加项目的时候显示没有项目等等。

68. 优化用户体验之自动排序

  func sortChecklists() {
    lists.sortInPlace({ checklist1, checklist2 in
      return checklist1.name.localizedStandardCompare(checklist2.name) == .OrderedAscending
    })
  }

这个方法的好处是,如果用户是用中文的,排序按照拼音的顺序排序,如果是英文,则按照英文从a到z的顺序来。用户使用的语言不对,相应的顺序也是不一样的。

真正的排序公式是:

checklist1.name.localizedStandardCompare(checklist2.name) == .OrderedAscending

如果你想按其他方式排序,只需要改动这一行代码即可。

当然,还要在下载读取plist文件(也就是在loadChecklists()方法里)的时候,调用这一方法

69. 优化用户体验之增加选择图片

允许用户给每个list选择一个图标,实际效果如下:

当然了,创建编辑list的时候,也需要增加图标这个选项:

将设计好的图片放入Xcode,在 Checklist.swift 文件里增加iconName变量,声明,初始化,NSCdoing 的两个协议方法,都要加上。然后到AllListsViewController、ListDetailViewController里把对应的图片显示出来(一个是所有清单列表的界面,一个是增加或者编辑清单的界面)。在增加或编辑清单界面中点击第二行cell,跳转到之后选择图标界面,Segue的Identifier是“PickIcon”,这个界面(IconPickerViewController.swift)需要先创建。

当用户没有选择图标时,实际上显示的是一张完全透明的图片,这样的效果就是下图中右侧的情况:


新建选择图片的swift文件:IconPickerViewController.swift,storyboard中拖入一个tableview Controller,关联,代码如下。

import UIKit

protocol IconPickerViewControllerDelegate: class {
  func iconPicker(picker: IconPickerViewController, didPickIcon iconName: String)
}

class IconPickerViewController: UITableViewController {
  weak var delegate: IconPickerViewControllerDelegate?

  let icons = [
    "No Icon",
    "Appointments",
    "Birthdays",
    "Chores",
    "Drinks",
    "Folder",
    "Groceries",
    "Inbox",
    "Photos",
    "Trips" ]

  override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return icons.count
  }
  
  override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("IconCell", forIndexPath: indexPath)
    
    let iconName = icons[indexPath.row]
    cell.textLabel!.text = iconName
    cell.imageView!.image = UIImage(named: iconName)
    
    return cell
  }
  
  override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    if let delegate = delegate {
      let iconName = icons[indexPath.row]
      delegate.iconPicker(self, didPickIcon: iconName)
    }
  }
}


然后回到增加或者编辑清单界面的view controller里补上IconPickerViewController.swift里创建的delegate的有关的代码,别忘了在类的开头写上:IconPickerViewControllerDelegate

下面代码都是因为增加了图标而做出了相应修改的代码:

  override func viewDidLoad() {
    super.viewDidLoad()
    
    if let checklist = checklistToEdit {
      title = "Edit Checklist"
      textField.text = checklist.name
      doneBarButton.enabled = true
      iconName = checklist.iconName
    }
    
    iconImageView.image = UIImage(named: iconName)
  }
  
  @IBAction func done() {
    if let checklist = checklistToEdit {
      checklist.name = textField.text!
      checklist.iconName = iconName
      delegate?.listDetailViewController(self, didFinishEditingChecklist: checklist)
    } else {
      let checklist = Checklist(name: textField.text!, iconName: iconName)
      delegate?.listDetailViewController(self, didFinishAddingChecklist: checklist)
    }
  }
  
  override func tableView(tableView: UITableView, willSelectRowAtIndexPath indexPath: NSIndexPath) -> NSIndexPath? {
    if indexPath.section == 1 {
      return indexPath
    } else {
      return nil
    }
  }
  
  override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == "PickIcon" {
      let controller = segue.destinationViewController as! IconPickerViewController
      controller.delegate = self
    }
  }
  
  func iconPicker(picker: IconPickerViewController, didPickIcon iconName: String) {
    self.iconName = iconName
    iconImageView.image = UIImage(named: iconName)
    navigationController?.popViewControllerAnimated(true)
  }
}

上面的代码有好多行,貌似是一项巨大工程,其实主要是设计的方法有些多,所以容易落下某个代码没有改。总结起来,就三件事情:

  • 增加一个新的 view controller 对象。
  • 在Storyboard中设计界面(还需要做一些自动布局约束的工作)
  • 然后用 segue 和 delegate 将这个新创建的 view controller 对象连接到 增加编辑清单 界面。

70. 优化用户体验之优化界面+适配机型

作者在优化外表的时候走了一个快捷方式,使用 tint color。tint color 能改变哪些地方呢?见下图:

而且更改 tint color 的方法非常简单,无需在Storyboard中一个一个界面的改,只要改掉 Global Tint 即可。如下图:

这样全局的 tint color 都是你想要的颜色啦多简单,一步到位

还有一个改进的地方:启动的时候,显示App的一部分,这样给用户一种错觉,App很快就启动起来了。其实现国外的App这样的设计比较多,国内的App都不这样做了,都会弄个广告啊,或者图片啊什么的。不过,也了解一下如何实现吧:

至于书中说的适配所有机型,其实只修改了 TextField 控件,只要这个控件在不同的机型下显示有些问题。

71. 函数式编程

近年来,函数式编程日趋流行。使用函数式编程有好处,可以缩短代码量。不过对于新手来说,可能阅读代码的时候会有些不习惯,不过慢慢习惯了就好了。
比如:

  func countUncheckedItems() -> Int {
    var count = 0
    for item in items where !item.checked {
      count += 1
    }
    return count
  }

用函数式编程写出来就是

func countUncheckedItems() -> Int {
    return items.reduce(0) { cnt, item in cnt + (item.checked ? 0 : 1) }
  }

reduce()这个方法,每次看到一个item就执行一遍{}里的代码。cnt变量一开始的值是0,每次根据item的情况来加1或0.

72. Convenience Initializer

  convenience init(name: String) {
    self.init(name: name, iconName: "No Icon")
  }
  
  init(name: String, iconName: String) {
    self.name = name
    self.iconName = iconName
    super.init()
  }

好吧,这个地方我实际上没太看懂,先复制一下原文,等哪天理解了,再总结一下。

Instead of super.init() it now calls self.init(name, iconName). Because it farms out its work to another init method, init(name) is now known as a convenience initializer. It does the same thing as init(name, iconName) but saves you from having to type iconName: "No Icon" whenever you want to use it.

init(name, iconName) has become the so-called designated initializer for Checklist. It is the primary way to create new Checklist objects, while init(name) exists only for the convenience of lazy developers... such as you and me. :-)

73. 本地提醒功能(local notifications)

首先要了解的一点是,这是 local notifications,不是开发时常常用到的 push notifications,push notifications可以让你的 App 接收外部的事件,比如新闻推送某只球队进了世界杯。

local notifications 更有点像是闹钟,用户在使用 App 的时候,设置了一个时间点,到点就会提醒。提醒的前提是,应用已经在后台运行,或者应用没有启动,如果应用正在使用,本地通知是不会显示的。这时候需要用其他方法另作处理。

在 iOS8 之后,只有在获得用户的允许后才能发送 local notifications,如果用户拒绝,到点也不会出现提醒信息。获取用户许可的方法在后面会说。

(1)扩展 data model

增加了提醒意味着数据也要增加相对应的属性,因为每个清单里的一个item里有提醒,所以要做ChecklistItem文件里增加属性。不过 UILocalNotification对象是无法存入plist文件中的,需要找替代方案,比如数字标识符:numeric primary。

  var dueDate = NSDate()
  var shouldRemind = false
  var itemID: Int

接着在 NSCoding 的2个协议方法增加相应的代码。在Objective-C语言中,Int,Float和Bool是原始类型,所以会看到shouldRemindcheckeditemID的方法有别与非原始类型的对象。

  required init?(coder aDecoder: NSCoder) {
    text = aDecoder.decodeObjectForKey("Text") as! String
    checked = aDecoder.decodeBoolForKey("Checked")
    dueDate = aDecoder.decodeObjectForKey("DueDate") as! NSDate
    shouldRemind = aDecoder.decodeBoolForKey("ShouldRemind")
    itemID = aDecoder.decodeIntegerForKey("ItemID")
    super.init()
  }
  
  func encodeWithCoder(aCoder: NSCoder) {
    aCoder.encodeObject(text, forKey: "Text")
    aCoder.encodeBool(checked, forKey: "Checked")
    aCoder.encodeObject(dueDate, forKey: "DueDate")
    aCoder.encodeBool(shouldRemind, forKey: "ShouldRemind")
    aCoder.encodeInteger(itemID, forKey: "ItemID")
  }

不要忘了更新初始化方法,注意代码中DataModel是大写开头的。

  override init() {

    //注意这里是大写开头的DataModel
    itemID = DataModel.nextChecklistItemID()
    super.init()
  }

在初始化方法里写这么一行代码是什么意思呢?不管什么时候只要App创建了一个新的 ChecklistItem 对象后,都让 DataModel 对象生成一个新的item ID。

之所以可以大写开头,是因为我们要在DataModel类中创建一个类方法(方法前面有个class,详细内容见#74):

  class func nextChecklistItemID() -> Int {
    let userDefaults = NSUserDefaults.standardUserDefaults()
    //读取出来值
    let itemID = userDefaults.integerForKey("ChecklistItemID")
    //写入新值
    userDefaults.setInteger(itemID + 1, forKey: "ChecklistItemID")
    //保存同步
    userDefaults.synchronize()
    return itemID
  }

NSUserDefaults里面没有ChecklistItemID这个键啊,是的,没有,所以需要加入:

  func registerDefaults() {
    let dictionary = [ "ChecklistIndex": -1,
                       "FirstTime": true,
                       "ChecklistItemID": 0 ]

    NSUserDefaults.standardUserDefaults().registerDefaults(dictionary)
  }
  

(2)搭建界面

先上效果图:


提醒时间功能

这个直接去控件库里拖动相对应的控件即可,没啥可说的,别忘了自动布局的约束。而下图这个就有些问题了,作者提供的方法非常新颖,我使用之后发现有个不好的地方,就是扩展性不够好,没法扩展到多个时间选择器上,比如这里只要提醒时间,要是我再加入开始时间和结束时间,不好实现同样的效果,扩展性不够。


时间选择器

步骤如下:
步骤一,把cell拖到下图中箭头所在的位置:

拖拽之后会出现这样的效果:


cell高度设为217,拖入一个DatePicker控件,效果如下:

(3)对应的Controller里编写代码:

还要创建Outlet连接:

  @IBOutlet weak var shouldRemindSwitch: UISwitch!
  @IBOutlet weak var dueDateLabel: UILabel!
  @IBOutlet weak var datePickerCell: UITableViewCell!
  @IBOutlet weak var datePicker: UIDatePicker!

声明新的变量:

  var dueDate = NSDate()
  var datePickerVisible = false

更新viewDidLoad()中的方法:编辑状态下2个属性要加入+更新提醒日期的Label内容(updateDueLabel()):

  override func viewDidLoad() {
    super.viewDidLoad()

    if let item = itemToEdit {
      title = "Edit Item"
      textField.text = item.text
      doneBarButton.enabled = true
      //新增加的两行代码:
      shouldRemindSwitch.on = item.shouldRemind
      dueDate = item.dueDate
    }
    //需要创建的方法:
    updateDueDateLabel()
  }

updateDueLabel()方法代码:

  func updateDueDateLabel() {
    let formatter = NSDateFormatter()
    formatter.dateStyle = .MediumStyle
    formatter.timeStyle = .ShortStyle
    dueDateLabel.text = formatter.stringFromDate(dueDate)
  }

NSDateFormatter可以改变时间显示的格式。
@IBAction func done()方法里也要增加新增的2个属性。

下面的代码都是有关如何显示时间选择器cell的:
看完这些方法,你就会理解我为什么说作者提供的这个方法非常麻烦了。

  1. cellForRowAtIndexPath
  override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    if indexPath.section == 1 && indexPath.row == 2 {
      return datePickerCell
    } else {
      return super.tableView(tableView, cellForRowAtIndexPath: indexPath)
    }
  }

2)numberOfRowsInSection

  override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if section == 1 && datePickerVisible {
      return 3
    } else {
      return super.tableView(tableView, numberOfRowsInSection: section)
    }
  }

3)heightForRowAtIndexPath

  override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    if indexPath.section == 1 && indexPath.row == 2 {
      return 217
    } else {
      return super.tableView(tableView, heightForRowAtIndexPath: indexPath)
    }
  }

4)didSelectRowAtIndexPath

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

    if indexPath.section == 1 && indexPath.row == 1 {
      if !datePickerVisible {
        showDatePicker()
      } else {
        hideDatePicker()
      }
    }
  }

5) willSelectRowAtIndexPath

  override func tableView(tableView: UITableView, willSelectRowAtIndexPath indexPath: NSIndexPath) -> NSIndexPath? {
    if indexPath.section == 1 && indexPath.row == 1 {
      return indexPath
    } else {
      return nil
    }
  }

6)indentationLevelForRowAtIndexPath

  override func tableView(tableView: UITableView, var indentationLevelForRowAtIndexPath indexPath: NSIndexPath) -> Int {
    if indexPath.section == 1 && indexPath.row == 2 {
      indexPath = NSIndexPath(forRow: 0, inSection: indexPath.section)
    }
    return super.tableView(tableView, indentationLevelForRowAtIndexPath: indexPath)
  }

到此结束了。但这样只能让cell一直出现,时间选择器会一直在这里出现的,这可不是我们想要的效果。我们需要点击一下显示,再点击一下隐藏,那么好吧,继续写代码吧~

下面这些代码有关显示隐藏时间选择器

1)显示时间选择器

  func showDatePicker() {
    datePickerVisible = true
    
    let indexPathDateRow = NSIndexPath(forRow: 1, inSection: 1)
    let indexPathDatePicker = NSIndexPath(forRow: 2, inSection: 1)
    
    if let dateCell = tableView.cellForRowAtIndexPath(indexPathDateRow) {
      dateCell.detailTextLabel!.textColor = dateCell.detailTextLabel!.tintColor
    }
    
    tableView.beginUpdates()
    tableView.insertRowsAtIndexPaths([indexPathDatePicker], withRowAnimation: .Fade)
    tableView.reloadRowsAtIndexPaths([indexPathDateRow], withRowAnimation: .None)
    tableView.endUpdates()
    
    datePicker.setDate(dueDate, animated: false)
  }

2)隐藏时间选择器

  func hideDatePicker() {
    if datePickerVisible {
      datePickerVisible = false
      
      let indexPathDateRow = NSIndexPath(forRow: 1, inSection: 1)
      let indexPathDatePicker = NSIndexPath(forRow: 2, inSection: 1)
      
      if let cell = tableView.cellForRowAtIndexPath(indexPathDateRow) {
        cell.detailTextLabel!.textColor = UIColor(white: 0, alpha: 0.5)
      }
      
      tableView.beginUpdates()
      tableView.reloadRowsAtIndexPaths([indexPathDateRow], withRowAnimation: .None)
      tableView.deleteRowsAtIndexPaths([indexPathDatePicker], withRowAnimation: .Fade)
      tableView.endUpdates()
    }
  }

优化用户体验
启动键盘后要隐藏日期选择器:

  func textFieldDidBeginEditing(textField: UITextField) {
    hideDatePicker()
  }

日期选择器中的时间每次发生改变后都更新Label内容:

  @IBAction func dateChanged(datePicker: UIDatePicker) {
    dueDate = datePicker.date
    updateDueDateLabel()
  }

(4)如何以及什么时候安排通知(schedule the notifications)

好吧,终于开始写有关通知的代码了,上面那么多东西,都是铺垫铺垫铺垫。。。
先创建一个方法,用来创建通知:

func scheduleNotification() {

}

这个方法写入ChecklistItem.swift这个文件里。

关于通知的时间,要注意,过去的时间点是无法提醒的,但是用户设置了过去的时间怎么办呢?

dueDate.compare(NSDate()) = .OrderedAscending

.OrderedAscending表示 dueDate 的时间在前,现在时间NSDate()在后,也就是说,提醒时间发生在过去。只有将时间设置到将来才可以提醒,所以设置提醒的条件为:!=,当然了,还要用户开启提醒功能:

if shouldRemind && dueDate.compare(NSDate()) != .OrderedAscending {
}

补充一下,NSComparisonResult 的结果,也就是 A.compare(B) 结果有三种:

  • .OrderedAscending,也是我们正在使用的,A发生在B之前(A在过去,B在将来)。
  • .OrderedSame,表示两个时间一致,A和B时间是同一个时间点。
  • .OrderedDescending,表示A发生在B之后,也就是先经过B时间点,然后才是A时间点。

当然了,每创建一个新的 Item 或者编辑一个 Item 的时候,都要检查一下是否要创建通知,所以:

  @IBAction func done() {
    if let item = itemToEdit {
      item.text = textField.text!
      item.shouldRemind = shouldRemindSwitch.on
      item.dueDate = dueDate
      item.scheduleNotification()
      delegate?.itemDetailViewController(self, didFinishEditingItem: item)
      
    } else {
      let item = ChecklistItem()
      item.text = textField.text!
      item.checked = false
      item.shouldRemind = shouldRemindSwitch.on
      item.dueDate = dueDate
      item.scheduleNotification()
      delegate?.itemDetailViewController(self, didFinishAddingItem: item)
    }
  }

现在可以创建通知了,创建本地通知有7行非常关键的代码:

  • 创建本地通知
  • 创建提醒时间
  • 创建时间提醒的时区
  • 创建通知显示的文案或内容
  • 创建通知使用的声音
  • 通知提醒的是哪个内容(哪个item)
  • 将创建好的本地通知排入时间表

用代码表示也就是:

      let localNotification = UILocalNotification()
      localNotification.fireDate = dueDate
      localNotification.timeZone = NSTimeZone.defaultTimeZone()
      localNotification.alertBody = text
      localNotification.soundName = UILocalNotificationDefaultSoundName
      localNotification.userInfo = ["ItemID": itemID]
      UIApplication.sharedApplication().scheduleLocalNotification(localNotification)

注意第一行代码我们实际上创建的是一个UILocalNotification对象,要使用这个对象,需要先 import UIKit。

当然我们要获取用户的许可才能设置通知,处理方法就是,当用户将switch按钮调整到on的时候,向用户获取许可,Ctrl拖拽创建Action连接:

  @IBAction func shouldRemindToggled(sender: UISwitch) {
    //隐藏键盘
    textField.resignFirstResponder()
    
    if sender.on {
      let notificationSettings = UIUserNotificationSettings(forTypes: [.Alert , .Sound], categories: nil)
      UIApplication.sharedApplication().registerUserNotificationSettings(notificationSettings)
    }
  }

当我们设置时间的时,会记录到秒,比如提醒时间是10:16:54,实际上我们在设置时间的时候,时间选择器里只显示到了分,但是提醒的时候,却会连秒也考虑到里面。如果想提高用户体验,去掉秒,直接在0秒的时候就提醒,那么,这是另外一个话题了。如何解决呢?作者也没说。

通知可能发生变化的五种情形:

  • 当用户新建一个 ChecklistItem 对象后,将 ShouldRemind 转换按钮调整到了开上,需要安排一个新的通知(相当于新建通知)。
  • 当用户改变提醒日期后,旧的通知需要取消,然后更换上新的通知日期。
  • 当用户将 ShouldRemind 转换按钮调整到关的状态后,当前的通知需要取消掉。
  • 当用户删除当前的 ChecklistItem 后,该Item下的通知需要被取消。
  • 当用户删除一整个 Checklist 后,里面所有的Item下的通知都需要被取消。

我们上面的各种步骤已经完成了新建通知,那么编辑和删除如何进行呢?

先说编辑,当我们编辑一个item的时候,先看是否存在一个通知,如果存在,取消通知即可,然后新建。每次编辑,都相当于删除旧的,重新创建新的通知,如果用户在编辑的时候没有修改通知,顶多就是新建了一个和之前一模一样的通知而已。

  func notificationForThisItem() -> UILocalNotification? {
    let allNotifications = UIApplication.sharedApplication().scheduledLocalNotifications!
    for notification in allNotifications {
      if let number = notification.userInfo?["ItemID"] as? Int where number == itemID {
        return notification
      }
    }
    return nil
  }

判断此item是否有通知。然后放入创建通知的方法里:

  func scheduleNotification() {
    let existingNotification = notificationForThisItem()
    if let notification = existingNotification {
      
      UIApplication.sharedApplication().cancelLocalNotification(notification)
    }
    
    if shouldRemind && dueDate.compare(NSDate()) != .OrderedAscending {
      let localNotification = UILocalNotification()
      localNotification.fireDate = dueDate
      localNotification.timeZone = NSTimeZone.defaultTimeZone()
      localNotification.alertBody = text
      localNotification.soundName = UILocalNotificationDefaultSoundName
      localNotification.userInfo = ["ItemID": itemID]
      
      UIApplication.sharedApplication().scheduleLocalNotification(localNotification)
      
     
    }
  }

最后剩下的就是删除了,即通知可能发生变化的五种情形的最后两种情绪,用一个方法即可解决:

  deinit {
    if let notification = notificationForThisItem() {
 
      UIApplication.sharedApplication().cancelLocalNotification(notification)
    }
  }

当年删除单个的ChecklistItem或者一个整个Checklist时,都会调用上面方法。

74. Class method vs. instance method(类方法和实例方法)

class func nextChecklistItemID()

class 这个关键词意味着你可以直接调用该方法,不用创建一个 DataMode 的对象的引用。

之前用的都是 instance method,只使用于类中某些实例。那什么时候用类方法什么时候用引用方法呢?哪个方法用的代码更少就用哪个方法啦,毕竟在这个App里,就这么一个地方用到了DataModel,如果以后你需要更新App,增加更多功能,很有可能就需要使用创建引用类型,然后使用引用方法。

更多详细的介绍会在下一本书中讲解,所以留着点疑问去看下一本书吧~

结束了

终于总结完毕了,终于看完了,本来以为一个周就能搞定,结果来来回回拖了这么久才完成,我也是小瞧了这本书,本来以为是100米短跑,跑完5000米之后才发现,前面还有一个马拉松。。。这就是我总结这本书的感受,再也不敢随意估算时间了,以后自己估计时间了之后再乘以3,如果完全没做过,乘以7,就是实际时间了。

照着书敲三遍代码,和总结一遍知识点,完全不是一个量级上的工作,虽然中间数次想放弃,好歹也走到了今天。有了这本书的知识,我完全可以开发一个无网络交互的本地App了,最起码数据持久化可以搞定了。要是哪天App上线了,我要在这里更新,多说几句~~

能看完的都是,我很佩服,总之,看到错别字或者错误的知识点,还请指正~~

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

推荐阅读更多精彩内容