第十章——UITableView 和 UITableViewController【譯】

許多 iOS 應用程序向用戶顯示列表項,并允許用戶選擇,刪除或重新排列列表項。 不管是顯示用戶地址簿中的人員列表的應用程序還是 App Store 上暢銷產品的列表,都使用了 UITableView。

UITableView 顯示具有可變數量行的單列數據。 圖10.1顯示了一些 UITableView 的例子。

圖10.1 UITableView示例

開始開發 Homepwner 應用程序

在本章中,您將啟動一個名為 Homepwner 的應用程序,該應用程序保留了所有財產的清單。 在發生火災或其他災難的情況下,您將有保險公司的記錄。 (“Homepwner”)順便說一下,不是打字錯誤,如果您需要 “pwn” 一詞的定義,請訪問 www.wiktionary.org。)

到目前為止,您的 iOS 項目一直很小,但是 Homepwner 將在八章中成長為一個很復雜的應用程序。 在本章結尾,Homepwner 將在 UITableView 中顯示 Item 實例列表,如圖10.2所示。

圖10.2 Homepwner:階段1

首先,請打開 Xcode 并創建一個新的 iOS Single View Application 項目。 配置如圖10.3所示。

圖10.3 配置Homepwner

UITableViewController

UITableView 是一個視圖對象。 回想一下,每個 iOS 開發人員盡力遵循 MVC 設計模式,每個類都屬于以下類別之一:

  • model:保存數據,不了解UI
  • view:對用戶可見,對模型對象一無所知
  • controller:保持UI和模型對象同步并控制應用程序的流程

作為視圖對象,UITableView 不處理應用程序邏輯或數據。 使用 UITableView 時,您必須考慮在應用程序中使表工作所需的其他功能:

  • UITableView 通常需要一個視圖控制器來處理其在屏幕上的外觀。
  • UITableView 需要一個數據源。 UITableView 請求其數據源獲得要顯示的行數,要顯示在這些行中的數據以及使 UITableView 成為有用 UI 的其他東西。 沒有數據源,表視圖只是一個空容器。 只要符合 UITableViewDataSource 協議,UITableViewdataSource 可以是任何類型的對象。
  • UITableView 通常需要一個可以通知其他對象涉及到 UITableView 事件的 委托(delegate)。 只要符合 UITableViewDelegate 協議,委托可以是任何對象。

UITableViewController 類的實例可以滿足所有三個角色:視圖控制器,數據源和委托。

UITableViewControllerUIViewController 的一個子類,因此具有一個視圖。 UITableViewController 的視圖通常是 UITableView 的一個實例,UITableViewController 處理 UITableView 的準備和呈現。

UITableViewController 創建其視圖時,UITableViewdataSourcedelegate 屬性將自動設置為指向 UITableViewController(圖10.4)。

圖10.4 UITableViewController-UITableView 關系

子類化UITableViewController

您將在 Homepwner 中創建 UITableViewController 的子類。 創建一個名為 ItemsViewController 的新的 Swift 文件。 在 ItemsViewController.swift 中,定義一個名為 ItemsViewControllerUITableViewController 子類。

import Foundation
import UIKit

class ItemsViewController: UITableViewController {

}

打開 Main.storyboard。 您希望初始視圖控制器是一個表視圖控制器。 在畫布上選擇現有的 View Controller,然后按 Delete 刪除。 然后將 Table View Controller 從對象庫拖動到畫布上。 選擇 Table View Controller 后,打開其身份檢查器,將 class 更改為 ItemsViewController。 最后,打開 Items View Controller 的屬性檢查器,并選中 Is Initial View Controller。

構建并運行您的應用程序。 您應該看到一個空的表視圖,如圖10.5所示。 作為 UIViewController 的子類,UITableViewController 繼承了 view 屬性。 當第一次訪問此屬性時,將調用 loadView() 方法,該方法創建并加載視圖對象。 UITableViewController 的視圖始終是 UITableView 的一個實例,因此可以用 UITableViewControllerview 屬性來獲得明亮,有光澤和空的表視圖。

圖10.5 空的UITableView

您不再需要模板為您創建的 ViewController.swift 文件。 在項目導航器中選擇此文件,然后
Delete 刪除。

創建 Item 類

您的表視圖需要顯示一些行。 表格視圖中的每一行將顯示一個信息項,例如包含 名稱(name),序列號(serial number) 和 美元值(value in dollars)。

創建一個名為 Item 的新的Swift文件。 在 Item.swift 中,定義 Item 類并賦予它四個屬性。

import Foundation
import UIKit

class Item: NSObject {
??var name: String
??var valueInDollars: Int
??var serialNumber: String?
??let dateCreated: Date
}

Item 繼承自 NSObject。 NSObject 是大多數 Objective-C 類繼承的基類。 您所使用的所有 UIKit 類——UIView,UITextFieldUIViewController,僅舉幾例——直接或間接地從 NSObject 繼承。 當需要與運行時系統進行交互時,您自己的類通常需要從 NSObject 繼承。

請注意,serialNumber 是可選的字符串,因為 item 可能沒有序列號。 另外,請注意,沒有一個屬性具有默認值。 您將需要在指定的構造器中給它們賦值。

自定義構造器

您在第2章中了解了結構體的構造器。在結構體上的構造器非常簡單,因為結構體不支持繼承。 相反,類構造器支持繼承。

類可以有兩種構造器:指派構造器(designated initializers)便利構造器(convenience initializers)。

指派構造器 是該類的主要構造器。 每個類都至少有一個 指派構造器。 指派構造器 可確保類中的所有屬性都具有值。 一旦它確保了,指派初構造器 就會在其父類(如果有的話)上調用 指派構造器。

Item 類上實現一個新的 指派構造器,它設置所有屬性的初始值。

import UIKit

class Item: NSObject {
??var name: String
??var valueInDollars: Int
??var serialNumber: String?
??let dateCreated: Date

??init(name: String, serialNumber: String?, valueInDollars: Int) {
????self.name = name
????self.valueInDollars = valueInDollars
????self.serialNumber = serialNumber
????self.dateCreated = Date()

????super.init()
??}
}

該構造器接收 nameserialNumbervalueInDollars 參數。 由于參數名稱和屬性名稱相同,因此您必須使用 self 來區分屬性和參數。

既然您已經實現了自己的自定義構造器,那么您將丟失該類的默認構造器 init()。 當您的所有類的屬性都具有默認值,而不需要額外的工作來創建新實例時,默認的構造器很有用。 Item 類不符合此條件,因此您已經為該類聲明了一個自定義初始化器。

每個類必須至少有一個 指派構造器,但 便利構造器 是可選的。 你可以將 便利構造器 視為助手。 一個 便利構造器 總是在同一個類上調用另一個初始化器。 初始化器名稱前面的 convenience 關鍵字表示 便利構造器。

添加一個 便利構造器 到 Item 類中,用于隨機生成 item。

convenience init(random: Bool = false) {
??if random {
????let adjectives = ["Fluffy", "Rusty", "Shiny"]
????let nouns = ["Bear", "Spork", "Mac"]

????var idx = arc4random_uniform(UInt32(adjectives.count))
????let randomAdjective = adjectives[Int(idx)]

????idx = arc4random_uniform(UInt32(nouns.count))
????let randomNoun = nouns[Int(idx)]

????let randomName = "\(randomAdjective) \(randomNoun)"
????let randomValue = Int(arc4random_uniform(100))
????let randomSerialNumber = UUID().uuidString.components(separatedBy: "-").first!

????self.init(name: randomName,serialNumber: randomSerialNumber, valueInDollars: randomValue)
??} else {
??????self.init(name: "", serialNumber: nil, valueInDollars: 0)
??}
}

如果 randomtrue,則實例配置有隨機名稱,序列號和值。 (arc4random_uniform 函數返回 0(包括0) 和 作為參數傳遞的值(不包括該值)之間的隨機值。)請注意,在條件的兩個分支的末尾,您正在調用到 Item 的 指派構造器。 便利構造器 必須調用相同類型的另一個構造器,而 指派構造器 必須在其父類上調用 指派構造器。

Item 類已準備就緒了。 在下一部分中,您將在表視圖中顯示一個 Item 的實例數組。

UITableView 的數據源

在 Cocoa Touch(用于構建iOS應用程序的框架集合)中為 UITableView 提供數據行的過程與典型的 過程編程 不同。 在過程編程中,您可以告訴表格視圖應該顯示什么。 而在 Cocoa Touch 中,表視圖會詢問另一個對象——它的 dataSource——它應該顯示什么。 在當前情況下, ItemsViewController 是數據源,因此需要一種存儲 item 數據的方法。

您將使用一個數組來存儲 Item 實例,但是要注意。 保存 Item 實例的數組將被抽象為另一個對象 - 一個 ItemStore(圖10.6)。

圖10.6 Homepwner 對象圖

如果一個對象想要查看所有的 item,它會詢問包含它們的數組的 ItemStore。 在將來的章節中,store 將負責對數組執行操作,如重新排序,添加和刪除項目。 它還將負責從磁盤保存和加載 item。

創建一個名為 ItemStore 的新的 Swift 文件。 在 ItemStore.swift 中,定義 ItemStore 類并聲明一個屬性來存儲 Item 列表。

import Foundation
import UIKit

class ItemStore {
??var allItems = [Item]()
}

ItemStore 是一個 Swift 基類(base class) - 它不會繼承任何其他類。 與之前定義的 Item 類不同,ItemStore 不需要 NSObject 提供的任何行為。

ItemViewController 將在創建一個新的 Item 時 調用 ItemStore 的方法。 ItemStore 將強制創建對象并將其添加到 Item 的實例數組中。

ItemStore.swift 中,實現 createItem() 來創建并返回一個新的 Item

@discardableResult func createItem() -> Item {
??let newItem = Item(random: true)

??allItems.append(newItem)

??return newItem
}

@discardableResult 注釋意味著此函數的調用者可以自由地忽略調用此函數的結果。 看看下面的代碼列表來說明這個效果。

// This is OK
let newItem = itemStore.createItem()

// This is also OK; the result is not assigned to a variable
itemStore.createItem()

讓控制器訪問 ItemStore

在 ItemsViewController.swift 中, 添加一個 ItemStore 屬性。

class ItemsViewController: UITableViewController {

??var itemStore: ItemStore!
}

現在,您應該在 ItemsViewController 實例哪個位置設置此屬性呢? 應用程序首次啟動時,AppDelegateapplication(_:didFinishLaunchingWithOptions :) 方法被調用。 AppDelegateAppDelegate.swift 中聲明,顧名思義,該應用程序用作應用程序本身的委托。 它負責處理應用程序所經歷的狀態的變化。 您將在第16章中了解 AppDelegate 和應用程序所經歷的更多信息。

打開 AppDelegate.swift。 訪問 ItemsViewController (它將是窗口的 根視圖控制器(rootViewController)),并將其 ItemStore 屬性設置為 ItemStore 的新實例。

func application(_ application: UIApplication, didFinishLaunchingWithOptions
????launchOptions: [UIApplicationLaunchOptionsKey : Any]?) -> Bool {
??// Override point for customization after application launch.

??// Create an ItemStore
??let itemStore = ItemStore()

??// Access the ItemsViewController and set its item store
??let itemsController = window!.rootViewController as! ItemsViewController
??itemsController.itemStore = itemStore

??return true
}

最后,在 ItemStore.swift 中,實現 指派構造器 來添加五個隨機 item。

init() {
??for _ in 0..<5 {
????createItem()
??}
}

如果 createItem() 沒有用 @discardableResult 注釋,那么這個函數的調用就必須像這樣:

// Call the function, but ignore the result
let _ = createItem()

此時您可能會想知道為什么 itemStore 要在 ItemsViewController 外部設置。 為什么 ItemsViewController 實例本身沒有創建一個 store 的實例? 這種方法的原因是基于一個相當復雜的內容叫做 依賴反轉原則(dependency inversion principle)。 這個原則的基本目標是通過反轉它們之間的某些依賴關系來解耦應用程序中的對象。 這會生成更強大和可維護的代碼。

依賴反轉原則指出:

  1. 高級對象不應該依賴于低級對象。 兩者都應該依賴于抽象。
  2. 抽象不應該依賴細節。 細節應該取決于抽象。

Homepwner 中的依賴反轉原則所要求的抽象是 store 的概念。一個 store 是一個較低級別的對象,它通過只有該類知道的細節來檢索和保存 Item 實例。 ItemsViewController 是一個更高級別的對象,只知道它將被提供一個實用程序對象(store),從該對象可以獲取 Item 實例的列表,并且它可以通過新的或更新的 Item 實例來持久存儲。 這將導致解耦,因為 ItemsViewController 不依賴于 ItemStore。 實際上,只要 store 是抽象的,ItemStore 可以被另外一個不同的 Item 對象取代(例如通過使用 Web服務),而 ItemViewController 不需要任何的修改。

實現依賴反轉原則時使用的常用模式是 依賴注入(dependency injection)。 在最簡單的形式中,較高級別的對象不會假定他們需要使用哪些較低級的對象。 相反,它們是通過構造器或屬性來傳遞的。 在 ItemsViewController 的實現中,您通過一個屬性來注入它來給它一個 store。

實現數據源方法

現在 store 里有一些 item,你需要在 ItemViewController 中將這些 item 轉換成 UITableView 可以顯示的行。 當 UITableView 想知道要顯示的內容時,它會從 UITableViewDataSource 協議中聲明的一組方法中調用方法。

打開文檔并搜索 UITableViewDataSource 協議參考。 向下滾動到 Configuring a Table View 部分(圖10.7)。

圖10.7 UITableViewDataSource協議文檔

Configuring a Table View 部分中,請注意,其中兩個方法標記為 Required。 因為 ItemsViewController 符合 UITableViewDataSource,所以它必須實現 tableView(_:numberOfRowsInSection :)tableView(_:cellForRowAt :)。 這些方法告訴表視圖應該顯示多少行以及在每行中顯示的內容。

每當 UITableView 顯示時,它會在其 dataSource 上調用一系列方法(必需的方法以及已經實現的任何可選的方法)。 必需的方法 tableView(_:numberOfRowsInSection :) 返回 UITableView 應顯示的行數的整數值。 在 Homepwner 的表視圖中,store 中的每個 item 應該為一行。

ItemsViewController.swift 中,實現 tableView(_:numberOfRowsInSection:)。

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
??return itemStore.allItems.count
}

想知道關于這個方法所指的 section 嗎? 表視圖可以分為幾個部分,每個部分都有自己的一組行。 例如,在通訊錄中,以 “C” 開頭的所有名稱在一個 section 中。 默認情況下,一個表視圖會有一個 section,在本章中,您將只使用一個。 一旦您了解表視圖的工作原理,就不難使用多個 section。 實際上,使用多個 section 是本章結尾的第一個挑戰。

UITableViewDataSource 協議中的第二個必需方法是 tableView(_:cellForRowAt :)。 要實現此方法,您需要了解另一個類——UITableViewCell。

UITableViewCells

表視圖的每一行都是一個視圖。 這些視圖是 UITableViewCell 的實例。 在本節中,您將創建 UITableViewCell 的實例來填充表視圖。

一個 單元(cell) 本身有一個子視圖——它的 contentView(圖10.8)。 contentView 是 cell 內容的父視圖。 cell 還可以具有 附件視圖(accessory view)。

圖10.8 UITableViewCell 布局

附件視圖顯示面向操作的圖標,例如復選標記,公開圖標或信息按鈕。 這些圖標可通過預定義的常數來修改附件視圖的外觀。 默認值為 UITableViewCellAccessoryType.none,這是本章要使用的。 您將在第23章再次看到附件視圖(現在感興趣的話請參閱 UITableViewCell 的文檔以了解更多詳細信息。)

一個 UITableViewCell 的真正有用的是 contentView,它有三個子視圖(圖10.9)。 其中兩個子視圖是 UILabel 實例,它們是名為 textLabeldetailTextLabelUITableViewCell 的屬性。 第三個子視圖是一個稱為 imageViewUIImageView。 在本章中,您將使用 textLabeldetailTextLabel。

圖10.9 UITableViewCell層級

每個 cell 還有一個 UITableViewCellStyle,它決定了哪些子視圖被使用,以及它們在 contentView 中的位置。 這些樣式及其常量的示例如圖10.10所示。

圖10.10 UITableViewCellStyle:樣式和常量

創建和檢索 UITableViewCells

現在,每個 cell 將以 textLabel 的形式顯示 Itemname,并將 ItemitemInDollars 作為 detailTextLabel 顯示。 為了實現這一點,您需要實現 UITableViewDataSource 協議第二個必需的方法 tableView(_:cellForRowAt :)。 此方法將創建一個 cell,將其 textLabel 設置為 Itemname,將其 detailTextLabel 設置為 ItemvalueInDollars,并將其返回到 UITableView(圖10.11)。

圖10.11 檢索 UITableViewCell

你如何決定哪一個 Item 對應哪一個 cell? 發送給 tableView(_:cellForRowAt :) 的參數之一是 IndexPath,它具有兩個屬性: sectionrow。 當在數據源上調用此方法時,表視圖會詢問 “我可以在X,Y行中顯示一個單元格嗎?”,因為本練習中只有一個部分,您的實現只會涉及到索引路徑的行。

ItemsViewController.swift 中,實現 tableView(_:cellForRowAt :),使第 n 行顯示 allItems 數組中的第 n 個 item。

override func tableView(_ tableView: UITableView,cellForRowAt indexPath: IndexPath) -> UITableViewCell {
??// Create an instance of UITableViewCell, with default appearance
??let cell = UITableViewCell(style: .value1, reuseIdentifier: "UITableViewCell")

??// Set the text on the cell with the description of the item
??// that is at the nth index of items, where n = row this cell
??// will appear in on the tableview
??let item = itemStore.allItems[indexPath.row]

??cell.textLabel?.text = item.name
??cell.detailTextLabel?.text = "$\(item.valueInDollars)"

??return cell
}

現在構建并運行應用程序,您將看到一個 UITableView,其中包含了一個 item 列表。

重用 UITableViewCells

iOS 設備的內存量有限。 如果您在 UITableView 中顯示了數千個 item 的列表,那么您將有數千個 UITableViewCell 的實例。 大多數這些 cell 將不必要地占用內存。 畢竟,如果用戶無法在屏幕上看到一個 cell,那么該 cell 沒有理由占用內存。

為了節省內存并提高性能,您可以重用表視圖 cell。 當用戶滾動表格時,某些 cell 將在屏幕外移動。 將屏幕外的 cell 放入可用于再利用的 cell 中。 然后,數據源首先檢查 cell 池,而不是為每個請求創建一個全新的單元。 如果有一個未使用的 cell,數據源將使用新數據進行配置并將其返回給表視圖(圖10.12)。

圖10.12 UITableViewCell的可重用實例

有一個問題需要注意:有時 UITableView 會有不同類型的 cell。 有時候,您會將 UITableViewCell 子類化以創建特殊的外觀或行為。 然而,在可重用 cell 池周圍浮動的不同子類有可能會導致返回錯誤類型的單元格。 您必須確保返回的 cell 的類型,以便您可以確定其具有的屬性和方法。

請注意,您不需要關心從池中取出的 cell 是什么類型的,因為您將要更改 cell 內容。 您需要的是特定類型的 cell。 好消息是每個單元格都有一個類型為 StringreuseIdentifier 屬性。 當數據源向表視圖詢問可重用的 cell 時,它傳遞一個字符串,并說:“我需要一個具有這種重用標識符的 cell。”按照慣例,重用標識符通常是單元類的名稱。

要重用 cell,您需要使用表視圖注冊 cell 原型或類,以獲取特定的重用標識符。 您將注冊默認的 UITableViewCell 類。 你告訴表格視圖,“嘿,任何時候,我要求一個這個重用標識符的 cell,給我一個這個特定類的 cell?!?表視圖將從重用池給你一個 cell 或如果在重用池中沒有該類型的 cell 則實例化一個新的。

打開 Main.storyboard。 請注意,在表格視圖中有一個 Prototype Cells 的部分(圖10.13)。

圖10.13原型單元格

在這里,您可以配置相關表格視圖所需的不同類型的 cell。 如果您正在創建自定義 cell,那么您將在其中設置 cell 的界面。 ItemsViewController 只需要一種 cell,并且現在使用其中一種內置樣式就足夠了,因此您只需要在已經在畫布上的 cell 上配置一些屬性。

選擇原型 cell 并打開其屬性檢查器。 將 Style 更改為 Right Detail(對應于 UITableViewCellStyle.value1),并給它一個 UITableViewCellIdentifier(圖10.14)。

圖10.14表視圖 cell 屬性

接下來,在 ItemsViewController.swift 中,更新 tableView(_:cellForRowAt :) 來重用單元格。

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
??// Create an instance of UITableViewCell, with default appearance
??//let cell = UITableViewCell(style: .value1, reuseIdentifier: "UITableViewCell")

??// Get a new or recycled cell
??let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)

??...
}

dequeueReusableCell(withIdentifier:for :) 方法將檢查 cell 的池或隊列,以查看具有正確重用標識符的 cell 是否已存在。 如果是這樣,它會 “出隊(dequeue)” 那個 cell。 如果沒有現有 cell,將創建并返回一個新的 cell。

構建并運行應用程序。 應用程序的行為應該保持不變。 重新使用 cell 意味著您只需要創建少量的cell,這對內存的要求較低。 您的應用程序的用戶(及其設備)將會感謝您。

Content Insets

正如您在本章中一直運行應用程序一樣,您可能已經注意到,第一個表視圖 cell 處于狀態欄下方(圖10.15)。 您創建的應用程序的界面填滿了設備的整個窗口。 狀態欄(如果可見)都會放置在界面頂部,因此您的界面必須考慮狀態欄的位置。

圖10.15 表視圖 cell 與狀態欄重疊

要使表格視圖 cell 不會覆蓋狀態欄,您將在表視圖的頂部添加一些填充。 UITableViewUIScrollView 的子類,它從中繼承 contentInset 屬性。 您可以將 content insets 想象為滾動視圖的四條邊的 填充(padding)。

ItemsViewController.swift 中,覆蓋 viewDidLoad() 來更新表視圖 content inset。

override func viewDidLoad() {

??super.viewDidLoad()
??// Get the height of the status bar
??let statusBarHeight = UIApplication.shared.statusBarFrame.height

??let insets = UIEdgeInsets(top: statusBarHeight, left: 0, bottom: 0, right: 0)
??tableView.contentInset = insets
??tableView.scrollIndicatorInsets = insets
}

表視圖的頂部被賦予了與狀態欄高度相等的 content inset。 當表視圖滾動到頂部時,這將使內容顯示在狀態欄的下方。 滾動指示器也會顯示在狀態欄下方,因此您可以給他們相同的 inset,讓它們出現在狀態欄的正下方。

請注意,您可以訪問 ItemsViewController 上的 tableView 屬性以獲取表視圖。 該屬性從 UITableViewController 繼承,并返回控制器的表視圖。 雖然您可以通過訪問 UITableViewController 的視圖來獲取相同的對象,但是使用 tableView 會告訴編譯器返回的對象將是 UITableView 的一個實例。 因此,調用特定于 UITableView 的方法或訪問特定于 UITableView 的屬性不會產生錯誤。

構建并運行應用程序。 當表視圖滾動到頂部時,表視圖單元格內容不再和狀態欄重疊(圖10.16)。

圖10.16 具有合適的 content inset 的表視圖

青銅挑戰:多個 section

UITableView 顯示兩個 section—— 一個用于價值超過 $50 的 item,另一個用于剩余的 item。 在開始此挑戰之前,請復制包含項目的文件夾及其所有源文件。 然后在復制的項目中完成挑戰; 原項目要保留到以后的章節用。

白銀挑戰:固定行

使 UITableView 的最后一行顯示為文本 “No more items!” 且無論 store 中的 item 數量是多少(包括0項),都會顯示。

黃金挑戰:自定義表

使每一行的高度達到 60 點(point),除了白銀挑戰中最后一行應該保持 44 point。 然后,將除最后一行之外的每行的字體大小更改為 20 點。 最后,使 UITableView 的背景顯示一個圖像。 (要使此像素完美,您將需要一個正確大小的圖像,具體取決于您的設備,請參見第1章中的圖表)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容