數(shù)據(jù)持久化方案解析(十) —— UIDocument的數(shù)據(jù)存儲(chǔ)(三)

版本記錄

版本號(hào) 時(shí)間
V1.0 2019.08.25 星期日

前言

數(shù)據(jù)的持久化存儲(chǔ)是移動(dòng)端不可避免的一個(gè)問題,很多時(shí)候的業(yè)務(wù)邏輯都需要我們進(jìn)行本地化存儲(chǔ)解決和完成,我們可以采用很多持久化存儲(chǔ)方案,比如說plist文件(屬性列表)惭嚣、preference(偏好設(shè)置)饭望、NSKeyedArchiver(歸檔)、SQLite 3澈侠、CoreData劫侧,這里基本上我們都用過。這幾種方案各有優(yōu)缺點(diǎn),其中烧栋,CoreData是蘋果極力推薦我們使用的一種方式写妥,我已經(jīng)將它分離出去一個(gè)專題進(jìn)行說明講解。這個(gè)專題主要就是針對(duì)另外幾種數(shù)據(jù)持久化存儲(chǔ)方案而設(shè)立审姓。
1. 數(shù)據(jù)持久化方案解析(一) —— 一個(gè)簡單的基于SQLite持久化方案示例(一)
2. 數(shù)據(jù)持久化方案解析(二) —— 一個(gè)簡單的基于SQLite持久化方案示例(二)
3. 數(shù)據(jù)持久化方案解析(三) —— 基于NSCoding的持久化存儲(chǔ)(一)
4. 數(shù)據(jù)持久化方案解析(四) —— 基于NSCoding的持久化存儲(chǔ)(二)
5. 數(shù)據(jù)持久化方案解析(五) —— 基于Realm的持久化存儲(chǔ)(一)
6. 數(shù)據(jù)持久化方案解析(六) —— 基于Realm的持久化存儲(chǔ)(二)
7. 數(shù)據(jù)持久化方案解析(七) —— 基于Realm的持久化存儲(chǔ)(三)
8. 數(shù)據(jù)持久化方案解析(八) —— UIDocument的數(shù)據(jù)存儲(chǔ)(一)
9. 數(shù)據(jù)持久化方案解析(九) —— UIDocument的數(shù)據(jù)存儲(chǔ)(二)

源碼

1. Swift

首先看下代碼組織結(jié)構(gòu)

下面看一下xib中的內(nèi)容

接著就是看一下代碼了

1. DetailViewController.swift
import UIKit
import Photos
import AssetsLibrary

protocol DetailViewControllerDelegate: class {
  func detailViewControllerDidFinish(_ viewController: DetailViewController, with photoEntry: PhotoEntry?, title: String?)
}

class DetailViewController: UIViewController {
  weak var delegate: DetailViewControllerDelegate?
  var document: Document? {
    didSet {
      guard let doc = document else { return }
      title = doc.description
    }
  }
  
  @IBOutlet weak var addPhotoButton: UIButton!
  @IBOutlet weak var titleTextField: UITextField!
  @IBOutlet weak var fullImageView: UIImageView!
  
  private var newImage: UIImage?
  private var newThumbnailImage: UIImage?
  private var hasChanges = false
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    if PHPhotoLibrary.authorizationStatus() == .notDetermined  {
      PHPhotoLibrary.requestAuthorization { _ in }
    }
    
    openDocument()
  }
  
  private func showImagePicker() {
    let imagePicker = UIImagePickerController()
    guard UIImagePickerController.isSourceTypeAvailable(imagePicker.sourceType) else { return }
    
    imagePicker.sourceType = .photoLibrary
    imagePicker.allowsEditing = false
    imagePicker.delegate = self
    
    present(imagePicker, animated: true, completion: nil)
  }
  
  private func openDocument() {
    if document == nil {
      showImagePicker()
    }
    else {
      document?.open() {
        [weak self] _ in
        self?.fullImageView.image = self?.document?.photo?.mainImage
        self?.titleTextField.text = self?.document?.description
      }
    }
  }
  
  @IBAction func editPhoto(_ sender: Any) {
    showImagePicker()
  }
  
  @IBAction func donePressed(_ sender: Any) {
    var photoEntry: PhotoEntry?

    if let newImage = newImage, let newThumb = newThumbnailImage {
      photoEntry = PhotoEntry(mainImage: newImage, thumbnailImage: newThumb)
    }

    let hasDifferentPhoto = !newImage.isSame(photo: document?.photo?.mainImage)
    let hasDifferentTitle = document?.description != titleTextField.text
    hasChanges = hasDifferentPhoto || hasDifferentTitle

    guard let doc = document, hasChanges else {
      delegate?.detailViewControllerDidFinish(
        self,
        with: photoEntry,
        title: titleTextField.text
      )
      dismiss(animated: true, completion: nil)
      return
    }

    doc.photo = photoEntry
    doc.save(to: doc.fileURL, for: .forOverwriting) { [weak self] (success) in
      guard let self = self else { return }
      
      if !success { fatalError("Failed to close doc.") }
      
      self.delegate?.detailViewControllerDidFinish(
        self,
        with: photoEntry,
        title: self.titleTextField.text
      )
      self.dismiss(animated: true, completion: nil)
    }
  }
  
  @IBAction func dismiss(_ sender: Any) {
    dismiss(animated: true, completion: nil)
  }
}

extension DetailViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
  @objc func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
    guard let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else {
      return
    }

    let options = PHImageRequestOptions()
    options.resizeMode = .exact
    options.isSynchronous = true

    if let imageAsset = info[UIImagePickerController.InfoKey.phAsset] as? PHAsset {
      let imageManager = PHImageManager.default()

      imageManager.requestImage(
        for: imageAsset,
        targetSize: CGSize(width: 150, height: 150),
        contentMode: .aspectFill,
        options: options
      ) { (result, _) in
          self.newThumbnailImage = result
      }
    }

    fullImageView.image = image
    let mainSize = fullImageView.bounds.size
    newImage = image.imageByBestFit(for: mainSize)

    picker.dismiss(animated: true, completion: nil)
  }
  
  @objc func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
    picker.dismiss(animated: true, completion: nil)
  }
}

extension DetailViewController: UITextFieldDelegate {
  func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    textField.resignFirstResponder()
    return true
  }
}
2. ViewController.swift
import UIKit

private extension String {
  static let cellID = "PhotoKeeperCell"
}

class ViewController: UIViewController {
  private var selectedEntry: Entry?
  private var entries: [Entry] = []
  private lazy var localRoot: URL? = FileManager.default.urls(
    for: .documentDirectory,
    in: .userDomainMask).first
  private var selectedDocument: Document?

  @IBOutlet weak var tableView: UITableView!
  @IBOutlet weak var leftBarButtonItem: UIBarButtonItem!
  
  override func viewDidLoad() {
    super.viewDidLoad()
    refresh()
  }

  private func loadDoc(at fileURL: URL) {
    let doc = Document(fileURL: fileURL)
    doc.open { [weak self] success in
      guard success else {
        fatalError("Failed to open doc.")
      }

      let metadata = doc.metadata
      let fileURL = doc.fileURL
      let version = NSFileVersion.currentVersionOfItem(at: fileURL)
      
      doc.close() { success in
        guard success else {
          fatalError("Failed to close doc.")
        }
        
        if let version = version {
          self?.addOrUpdateEntry(for: fileURL, metadata: metadata, version: version)
        }
      }
    }
  }

  private func loadLocal() {
    guard let root = localRoot else { return }
    do {
      let localDocs = try FileManager.default.contentsOfDirectory(
        at: root,
        includingPropertiesForKeys: nil,
        options: [])

      for localDoc in localDocs where localDoc.pathExtension == .appExtension {
        loadDoc(at: localDoc)
      }
    } catch let error {
      fatalError("Couldn't load local content. \(error.localizedDescription)")
    }
  }

  private func refresh() {
    loadLocal()
    tableView.reloadData()
  }

  private func getDocumentURL(for filename: String) -> URL? {
    return localRoot?.appendingPathComponent(filename, isDirectory: false)
  }

  private func docNameExists(for docName: String) -> Bool {
    return !entries.filter{ $0.fileURL.lastPathComponent == docName }.isEmpty
  }

  private func indexOfEntry(for fileURL: URL) -> Int? {
    return entries.firstIndex(where: { $0.fileURL == fileURL })
  }

  private func addOrUpdateEntry(
    for fileURL: URL,
    metadata: PhotoMetadata?,
    version: NSFileVersion) {

    if let index = indexOfEntry(for: fileURL) {
      let entry = entries[index]
      entry.metadata = metadata
      entry.version = version
    } else {
      let entry = Entry(fileURL: fileURL, metadata: metadata, version: version)
      entries.append(entry)
    }

    entries = entries.sorted(by: >)
    tableView.reloadData()
  }

  private func insertNewDocument(
    with photoEntry: PhotoEntry? = nil,
    title: String? = nil) {

    guard let fileURL = getDocumentURL(
      for: getDocFilename(for: title ?? .photoKey)
      ) else { return }


    let doc = Document(fileURL: fileURL)
    doc.photo = photoEntry

    doc.save(to: fileURL, for: .forCreating) {
      [weak self] success in
      guard success else {
        fatalError("Failed to create file.")
      }

      print("File created at: \(fileURL)")

      let metadata = doc.metadata
      let URL = doc.fileURL
      if let version = NSFileVersion.currentVersionOfItem(at: fileURL) {
        self?.addOrUpdateEntry(for: URL, metadata: metadata, version: version)
      }
    }
  }

  private func showDetailVC() {
    guard let detailVC = detailVC else { return }

    detailVC.delegate = self
    detailVC.document = selectedDocument

    mode = .viewing
    present(detailVC.navigationController!, animated: true, completion: nil)
  }

  private func getDocFilename(for prefix: String) -> String {
    var newDocName = String(format: "%@.%@", prefix, String.appExtension)
    var docCount = 1

    while docNameExists(for: newDocName) {
      newDocName = String(format: "%@ %d.%@", prefix, docCount, String.appExtension)
      docCount += 1
    }

    return newDocName
  }
  
  private func indexOfEntry(for name: String) -> Int? {
    return entries.firstIndex(where: { $0.description == name})
  }

  
  @IBAction func addEntry(_ sender: Any) {
    selectedEntry = nil
    selectedDocument = nil
    showDetailVC()
  }
  
  @IBAction func editEntries(_ sender: Any) {
    mode = mode.otherMode
  }
  
  private func delete(entry: Entry) {
    let fileURL = entry.fileURL
    guard let entryIndex = indexOfEntry(for: fileURL) else { return }
    
    do {
      try FileManager.default.removeItem(at: fileURL)
      entries.remove(at: entryIndex)
      tableView.reloadData()
    } catch {
      fatalError("Couldn't remove file.")
    }
  }
  
  private func rename(_ entry: Entry, with name: String) {
    guard entry.description != name else { return }

    let newDocFilename = "\(name).\(String.appExtension)"

    if docNameExists(for: newDocFilename) {
      fatalError("Name already taken.")
    }

    guard let newDocURL = getDocumentURL(for: newDocFilename) else { return }

    do {
      try FileManager.default.moveItem(at: entry.fileURL, to: newDocURL)
    } catch {
      fatalError("Couldn't move to new URL.")
    }

    entry.fileURL = newDocURL
    entry.version = NSFileVersion.currentVersionOfItem(at: entry.fileURL) ?? entry.version

    tableView.reloadData()
  }
  
  private var mode: Mode = .viewing {
    didSet {
      switch mode {
      case .editing:
        tableView.setEditing(true, animated: true)
        leftBarButtonItem.title = "Done"
      case .viewing:
        tableView.setEditing(false, animated: true)
        leftBarButtonItem.title = "Edit"
      }
    }
  }
}

//MARK: DetailViewControllerDelegate
extension ViewController: DetailViewControllerDelegate {
  func detailViewControllerDidFinish(_ viewController: DetailViewController, with photoEntry: PhotoEntry?, title: String?) {
    guard
      let doc = viewController.document,
      let version = NSFileVersion.currentVersionOfItem(at: doc.fileURL)
      else {
        if let docData = photoEntry {
          insertNewDocument(with: docData, title: title)
        }
        return
    }


    if let docData = photoEntry {
      doc.photo = docData
    }

    addOrUpdateEntry(for: doc.fileURL, metadata: doc.metadata, version: version)
    if let title = title, let entry = selectedEntry, title != entry.description {
      rename(entry, with: title)
    }
  }
}

//MARK: UITableViewDelegate
extension ViewController: UITableViewDelegate {
  func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let entry = entries[indexPath.row]
    selectedEntry = entry
    selectedDocument = Document(fileURL: entry.fileURL)

    showDetailVC()

    tableView.deselectRow(at: indexPath, animated: false)
  }

  func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
    return true
  }
  
  func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
    
    if editingStyle == .delete {
      let entry = entries[indexPath.row]
      delete(entry: entry)
    }
  }
}

//MARK: UITableViewDataSource
extension ViewController: UITableViewDataSource {
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return entries.count
  }
  
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: .cellID, for: indexPath) as? PhotoKeeperCell else { return UITableViewCell() }
    
    let entry = entries[indexPath.row]
    
    cell.photoImageView?.image = entry.metadata?.image
    cell.titleTextField?.text = entry.description
    cell.subtitleLabel?.text = entry.version.modificationDate?.mediumString
    
    return cell
  }
}

//MARK: UITextFieldDelegate
extension ViewController: UITextFieldDelegate {
  func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    textField.resignFirstResponder()
    mode = .viewing
    
    guard
      let entry = selectedEntry,
      let newName = textField.text
      else {
        return true
    }
    
    rename(entry, with: newName)
    return true
  }
  
  func textFieldDidBeginEditing(_ textField: UITextField) {
    let filteredEntries = entries.filter { (entry) -> Bool in
      return entry.description == textField.text
    }
    
    guard let entry = filteredEntries.first else { return }
    
    selectedEntry = entry
  }
  
  func textFieldDidEndEditing(_ textField: UITextField) {
    textField.resignFirstResponder()
  }
}

//MARK: Additional Conveniences
extension ViewController {
  private var detailVC: DetailViewController? {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let detailNavVC = storyboard.instantiateViewController(withIdentifier: "DetailNavigationController")
    
    guard
      let navVC = detailNavVC as? UINavigationController,
      let detailVC = navVC.topViewController as? DetailViewController
      else {
        return nil
    }

    return detailVC
  }
}

private enum Mode {
  case editing
  case viewing
  
  var otherMode: Mode {
    switch self {
    case .editing:
      return .viewing
    case .viewing:
      return .editing
    }
  }
}
3. PhotoKeeperCell.swift
import UIKit

class PhotoKeeperCell: UITableViewCell {
  @IBOutlet weak var photoImageView: UIImageView!
  @IBOutlet weak var titleTextField: UITextField!
  @IBOutlet weak var subtitleLabel: UILabel!
  
  override func setSelected(_ selected: Bool, animated: Bool) {
    super.setSelected(selected, animated: animated)
  }
  
  override func setEditing(_ editing: Bool, animated: Bool) {
    super.setEditing(editing, animated: animated)
    
    UIView.animate(withDuration: 0.1) {
      self.titleTextField.isEnabled = editing
      self.titleTextField.borderStyle = editing ? UITextField.BorderStyle.roundedRect : .none
    }
  }
}
4. Date+Extensions.swift
import Foundation

private var mediumFormatter: DateFormatter = {
    let dateFormatter =  DateFormatter()
    dateFormatter.doesRelativeDateFormatting = true
    dateFormatter.timeStyle = .medium
    dateFormatter.dateStyle = .medium
    return dateFormatter
}()

extension Date {
  var mediumString: String {
    return mediumFormatter.string(from: self)
  }
}
5. UIImage+Extensions.swift
import UIKit

extension UIImage {
  static var `default`: UIImage {
    return #imageLiteral(resourceName: "default")
  }
  
  func imageWith(newSize: CGSize) -> UIImage {
    let renderer = UIGraphicsImageRenderer(size: newSize)
    let image = renderer.image {_ in
      draw(in: CGRect(origin: .zero, size: newSize))
    }
    
    return image
  }
  
  func imageByBestFit(for targetSize: CGSize) -> UIImage {
    let aspectRatio = size.width / size.height
    let targetHeight = targetSize.height
    let scaledWidth = targetSize.height * aspectRatio
    
    let bestSize = CGSize(width: scaledWidth, height: targetHeight)
    return imageWith(newSize: bestSize)
  }
}

extension Optional where Wrapped == UIImage {
  func isSame(photo: UIImage?) -> Bool {
    switch (self, photo) {
    case (nil, nil):
      return true
    case (nil, _), (_, nil):
      return false
    case (let p1, let p2):
      return p1!.isEqual(p2)
    }
  }
}
6. PhotoData.swift
import Foundation
import UIKit

class PhotoData: NSObject, NSCoding {
  var image: UIImage?

  init(image: UIImage? = nil) {
    self.image = image
  }

  func encode(with aCoder: NSCoder) {
    aCoder.encode(1, forKey: .versionKey)
    guard let photoData = image?.pngData() else { return }
    aCoder.encode(photoData, forKey: .photoKey)
  }

  required init?(coder aDecoder: NSCoder) {
    aDecoder.decodeInteger(forKey: .versionKey)
    guard let photoData = aDecoder.decodeObject(forKey: .photoKey) as? Data else { return nil }
    self.image = UIImage(data: photoData)
  }
}
7. PhotoMetadata.swift
import Foundation
import UIKit

class PhotoMetadata: NSObject, NSCoding {
  var image: UIImage?

  init(image: UIImage? = nil) {
    self.image = image
  }

  func encode(with aCoder: NSCoder) {
    aCoder.encode(1, forKey: .versionKey)

    guard let photoData = image?.pngData() else { return }
    aCoder.encode(photoData, forKey: .thumbnailKey)
  }

  required init?(coder aDecoder: NSCoder) {
    aDecoder.decodeInteger(forKey: .versionKey)

    guard let photoData = aDecoder.decodeObject(forKey: .thumbnailKey) as? Data else { return }
    image = UIImage(data: photoData)
  }
}
8. Entry.swift
import Foundation
import UIKit

class Entry: NSObject {
  var fileURL: URL
  var metadata: PhotoMetadata?
  var version: NSFileVersion

  private var editDate: Date {
    return version.modificationDate ?? .distantPast
  }

  override var description: String {
    return fileURL.deletingPathExtension().lastPathComponent
  }

  init(fileURL: URL, metadata: PhotoMetadata?, version: NSFileVersion) {
    self.fileURL = fileURL
    self.metadata = metadata
    self.version = version
  }
}

extension Entry: Comparable {
  static func < (lhs: Entry, rhs: Entry) -> Bool {
    return lhs.editDate < rhs.editDate
  }
}
9. Document.swift
import UIKit

extension String {
  static let appExtension: String = "ptk"
  static let versionKey: String = "Version"
  static let photoKey: String = "Photo"
  static let thumbnailKey: String = "Thumbnail"
}

typealias PhotoEntry = (mainImage: UIImage?, thumbnailImage: UIImage?)
private extension String {
  static let dataKey: String = "Data"
  static let metadataFilename: String = "photo.metadata"
  static let dataFilename: String = "photo.data"
}

class Document: UIDocument {
  override var description: String {
    return fileURL.deletingPathExtension().lastPathComponent
  }

  var fileWrapper: FileWrapper?

  lazy var photoData: PhotoData = {
    guard
      fileWrapper != nil,
      let data = decodeFromWrapper(for: .dataFilename) as? PhotoData
      else {
        return PhotoData()
    }
    
    return data
  }()
  
  lazy var metadata: PhotoMetadata = {
    guard
      fileWrapper != nil,
      let data = decodeFromWrapper(for: .metadataFilename) as? PhotoMetadata
      else {
        return PhotoMetadata()
    }
    
    return data
    
  }()
  
  // 4
  var photo: PhotoEntry? {
    get {
      return PhotoEntry(mainImage: photoData.image, thumbnailImage: metadata.image)
    }

    set {
      photoData.image = newValue?.mainImage
      metadata.image = newValue?.thumbnailImage
    }
  }

  private func encodeToWrapper(object: NSCoding) -> FileWrapper {
    let archiver = NSKeyedArchiver(requiringSecureCoding: false)
    archiver.encode(object, forKey: .dataKey)
    archiver.finishEncoding()

    return FileWrapper(regularFileWithContents: archiver.encodedData)
  }

  override func contents(forType typeName: String) throws -> Any {
    
    let metaDataWrapper = encodeToWrapper(object: metadata)
    let photoDataWrapper = encodeToWrapper(object: photoData)
    let wrappers: [String: FileWrapper] = [.metadataFilename: metaDataWrapper,
                                           .dataFilename: photoDataWrapper]

    return FileWrapper(directoryWithFileWrappers: wrappers)
  }

  override func load(fromContents contents: Any, ofType typeName: String?) throws {
    guard let contents = contents as? FileWrapper else { return }

    fileWrapper = contents
  }

  func decodeFromWrapper(for name: String) -> Any? {
    guard let allWrappers = fileWrapper,
      let wrapper = allWrappers.fileWrappers?[name],
      let data = wrapper.regularFileContents else { return nil }

    do {
      let unarchiver = try NSKeyedUnarchiver.init(forReadingFrom: data)
      unarchiver.requiresSecureCoding = false
      return unarchiver.decodeObject(forKey: .dataKey)
    } catch let error {
      fatalError("Unarchiving failed. \(error.localizedDescription)")
    }
  }
}

后記

本篇主要講述了UIDocument的數(shù)據(jù)存儲(chǔ)珍特,感興趣的給個(gè)贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市魔吐,隨后出現(xiàn)的幾起案子扎筒,更是在濱河造成了極大的恐慌,老刑警劉巖酬姆,帶你破解...
    沈念sama閱讀 216,744評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嗜桌,死亡現(xiàn)場離奇詭異,居然都是意外死亡辞色,警方通過查閱死者的電腦和手機(jī)骨宠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來相满,“玉大人层亿,你說我怎么就攤上這事×⒚溃” “怎么了棕所?”我有些...
    開封第一講書人閱讀 163,105評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長悯辙。 經(jīng)常有香客問我琳省,道長,這世上最難降的妖魔是什么躲撰? 我笑而不...
    開封第一講書人閱讀 58,242評(píng)論 1 292
  • 正文 為了忘掉前任针贬,我火速辦了婚禮,結(jié)果婚禮上拢蛋,老公的妹妹穿的比我還像新娘桦他。我一直安慰自己,他們只是感情好谆棱,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,269評(píng)論 6 389
  • 文/花漫 我一把揭開白布快压。 她就那樣靜靜地躺著,像睡著了一般垃瞧。 火紅的嫁衣襯著肌膚如雪蔫劣。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,215評(píng)論 1 299
  • 那天个从,我揣著相機(jī)與錄音脉幢,去河邊找鬼歪沃。 笑死,一個(gè)胖子當(dāng)著我的面吹牛嫌松,可吹牛的內(nèi)容都是我干的沪曙。 我是一名探鬼主播,決...
    沈念sama閱讀 40,096評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼萎羔,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼液走!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起贾陷,我...
    開封第一講書人閱讀 38,939評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤育灸,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后昵宇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體磅崭,經(jīng)...
    沈念sama閱讀 45,354評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,573評(píng)論 2 333
  • 正文 我和宋清朗相戀三年瓦哎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了砸喻。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,745評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蒋譬,死狀恐怖割岛,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情犯助,我是刑警寧澤癣漆,帶...
    沈念sama閱讀 35,448評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站剂买,受9級(jí)特大地震影響惠爽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜瞬哼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,048評(píng)論 3 327
  • 文/蒙蒙 一婚肆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧坐慰,春花似錦较性、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,683評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至糟港,卻和暖如春攀操,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背着逐。 一陣腳步聲響...
    開封第一講書人閱讀 32,838評(píng)論 1 269
  • 我被黑心中介騙來泰國打工崔赌, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人耸别。 一個(gè)月前我還...
    沈念sama閱讀 47,776評(píng)論 2 369
  • 正文 我出身青樓健芭,卻偏偏與公主長得像,于是被迫代替她去往敵國和親秀姐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子慈迈,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,652評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容