- 原文地址:iOS: How to build a Table View with multiple cell types
- 原文做者:Stan Ostrovskiy
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:LoneyIsError
- 校對者:Fengziyin1234
第1部分:怎樣才能不迷失在大量代碼中html
在具備靜態 Cell 的表視圖中,其 Cell 的數量和順序是恆定的。要實現這樣的表視圖很是簡單,與實現常規 UIView 沒有太大的區別。前端
只包含一種內容類型的動態 Cell 的表視圖:Cell 的數量和順序是動態變化的,但全部 Cell 都有相同類型的內容。在這裏你可使用可複用 Cell 。這也是最多見的表視圖樣式。android
包含具備不一樣內容類型的動態 Cell 的表視圖:數量,順序和 Cel l類型是動態的。實現這種表視圖是最有趣和最具挑戰性的。ios
想象一下這個應用程序,你必須構建這樣的頁面:git
全部數據都來自後端,咱們沒法控制下一個請求將接收哪些數據:可能沒有「about」的信息,或者「gallery」部分多是空的。在這種狀況下,咱們根本不須要展現這些 Cell。最後,咱們必須知道用戶點擊的 Cell 類型並作出相應的反應。github
首先,讓咱們來先肯定問題。express
我常常在不一樣項目中看到這樣的方法:在 UITableView 中根據 index 配置 Cell。編程
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
//configure cell type 1
} else if indexPath.row == 1 {
//configure cell type 2
}
....
}
複製代碼
一樣在代理方法 didSelectRowAt 中幾乎使用相同的代碼:json
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row == 0 {
//configure action when tap cell 1
} else if indexPath.row == 1 {
//configure action when tap cell 1
}
....
}
複製代碼
直到你想要從新排序 Cell 或在表視圖中刪除或添加新的 Cell 的那一刻,代碼都將如所預期的工做。若是你更改了一個 index,那麼整個表視圖的結構都將破壞,你須要手動更新 cellForRowAt 和 didSelectRowAt 方法中全部的 index。swift
換句話說,它沒法重用,可讀性差,也不遵循任何編程模式,由於它混合了視圖和 Model。
有什麼更好的方法嗎?
在這個項目中,咱們將使用 MVVM 模式。MVVM 表明「Model-View-ViewModel」,當你在模型和視圖之間須要額外的視圖時,這種模式很是有用。你能夠在此處閱讀有關全部主要 iOS 設計模式 的更多信息。
在本系列教程的第一部分中,咱們將使用 JSON 做爲數據源構建動態表視圖。咱們將討論如下主題和概念:協議,協議拓展,屬性計算,聲明轉換 以及更多。
在下一個教程中,咱們將把它提升一個難度:經過幾行代碼來實現 section 的摺疊。。
首先,建立一個新項目,將 TableView 添加到默認的 ViewController 中,ViewController 綁定該 tableView,並將ViewController 嵌入到 NavigationController 中,並確保項目能按預期編譯和運行。這是基本步驟,此處不予介紹。若是你在這部分遇到麻煩,那對你來講深刻研究這個話題可能太早了。
你的 ViewController 類應該像這樣子:
class ViewController: UIViewController {
@IBOutlet weak var tableView: UITableView?
override func viewDidLoad() {
super.viewDidLoad()
}
}
複製代碼
我建立了一個簡單的 JSON 數據,來模仿服務器響應。你能夠在個人 Dropbox 中下載它。將此文件保存在項目文件夾中,並確保該文件的項目名稱與文件檢查器中的目標名稱相同:
你還須要一些圖片,你能夠在 這裏 找到。下載存檔,解壓縮,而後將圖片添加到資源文件夾。不要對任何圖片重命名。
咱們須要建立一個 Model,它將保存咱們從 JSON 讀取的全部數據。
class Profile {
var fullName: String?
var pictureUrl: String?
var email: String?
var about: String?
var friends = [Friend]()
var profileAttributes = [Attribute]()
}
class Friend {
var name: String?
var pictureUrl: String?
}
class Attribute {
var key: String?
var value: String?
}
複製代碼
咱們將給 JSON 對象添加初始化方法,那樣你就能夠輕鬆地將 JSON 映射到 Model。首先,咱們須要從 .json 文件中提取內容的方法,並將其轉成 Data 對象:
public func dataFromFile(_ filename: String) -> Data? {
@objc class TestClass: NSObject { }
let bundle = Bundle(for: TestClass.self)
if let path = bundle.path(forResource: filename, ofType: "json") {
return (try? Data(contentsOf: URL(fileURLWithPath: path)))
}
return nil
}
複製代碼
使用 Data 對象,咱們能夠初始化 Profile 類。原生或第三方庫中有許多不一樣的方能夠在 Swift 中解析JSON,你可使用你喜歡的那個。我堅持使用標準的 Swift JSONSerialization 庫來保持項目的精簡,不使用任何第三方庫:
class Profile {
var fullName: String?
var pictureUrl: String?
var email: String?
var about: String?
var friends = [Friend]()
var profileAttributes = [Attribute]()
init?(data: Data) {
do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let body = json[「data」] as? [String: Any] {
self.fullName = body[「fullName」] as? String
self.pictureUrl = body[「pictureUrl」] as? String
self.about = body[「about」] as? String
self.email = body[「email」] as? String
if let friends = body[「friends」] as? [[String: Any]] {
self.friends = friends.map { Friend(json: $0) }
}
if let profileAttributes = body[「profileAttributes」] as? [[String: Any]] {
self.profileAttributes = profileAttributes.map { Attribute(json: $0) }
}
}
} catch {
print(「Error deserializing JSON: \(error)」)
return nil
}
}
}
class Friend {
var name: String?
var pictureUrl: String?
init(json: [String: Any]) {
self.name = json[「name」] as? String
self.pictureUrl = json[「pictureUrl」] as? String
}
}
class Attribute {
var key: String?
var value: String?
init(json: [String: Any]) {
self.key = json[「key」] as? String
self.value = json[「value」] as? String
}
}
複製代碼
咱們的 Model 已準備就緒,因此咱們須要建立 ViewModel。它將負責向咱們的 TableView 提供數據。
咱們將建立 5 個不一樣的 table sections:
前三個 section 各只有一個 Cell,最後兩個 section 能夠有多個 Cell,具體取決於咱們的 JSON 文件的內容。
由於咱們的數據是動態的,因此 Cell 的數量不是固定的,而且咱們對每種類型的數據使用不一樣的 tableViewCell,所以咱們須要使用正確的 ViewModel 結構。首先,咱們必須區分數據類型,以便咱們可使用適當的 Cell。當你須要在 Swift 中使用多種類型而且能夠輕鬆的切換時,最好的方法是使用枚舉。那麼讓咱們開始使用 ViewModelItemType 構建 ViewModel:
enum ProfileViewModelItemType {
case nameAndPicture
case about
case email
case friend
case attribute
}
複製代碼
每一個 enum case 表示 TableViewCell 須要的不一樣的數據類型。可是,我因爲們但願在同一個表視圖中使用數據,因此須要有一個單獨的 dataModelItem,它將決定全部屬性。咱們能夠經過使用協議來實現這一點,該協議將爲咱們的 item 提供屬性計算:
protocol ProfileViewModelItem {
}
複製代碼
首先,咱們須要知道的是 item 的類型。所以咱們爲協議建立一個類型屬性。當你建立協議屬性時,你須要爲該屬性設置 name, type,並指定該屬性是 gettable 仍是 settable 和 gettable。你能夠在 此處 得到有關協議屬性的更多信息和示例。在咱們的例子中,類型將是 ProfileViewModelItemType,咱們僅須要只讀該屬性:
protocol ProfileViewModelItem {
var type: ProfileViewModelItemType { get }
}
複製代碼
咱們須要的下一個屬性是 rowCount。它將告訴咱們每一個 section 有多少行。爲此屬性指定類型和只讀類型:
protocol ProfileViewModelItem {
var type: ProfileViewModelItemType { get }
var rowCount: Int { get }
}
複製代碼
咱們最好在協議中添加一個 sectionTitle 屬性。基本上,sectionTitle 也屬於 TableView 的相關數據。如你所知,在使用 MVVM 結構時,除了在 viewModel 中,咱們不但願在其餘任何地方建立任何類型的數據,:
protocol ProfileViewModelItem {
var type: ProfileViewModelItemType { get }
var rowCount: Int { get }
var sectionTitle: String { get }
}
複製代碼
如今,咱們已經準備好爲每種數據類型建立 ViewModelItem。每一個 item 都須要遵照協議。但在咱們開始以前,讓咱們再向簡潔有序的項目邁出一步:爲咱們的協議提供一些默認值。在 swift 中,咱們可使用協議擴展爲協議提供默認值:
extension ProfileViewModelItem {
var rowCount: Int {
return 1
}
}
複製代碼
如今,若是 rowCount 爲 1,咱們就沒必要爲 item 的 rowCount 賦值了,它將爲你節省一些冗餘的代碼。
協議擴展還容許您在不使用 @objc 協議的狀況下生成可選的協議方法。只需建立一個協議擴展並在這個擴展中實現默認方法。
先爲 nameAndPicture Cell 建立一個 ViewModeItem。
class ProfileViewModelNameItem: ProfileViewModelItem {
var type: ProfileViewModelItemType {
return .nameAndPicture
}
var sectionTitle: String {
return 「Main Info」
}
}
複製代碼
正如我以前所說,在這種狀況下,咱們不須要爲 rowCount 賦值,由於,咱們只須要默認值 1。
如今咱們添加其餘屬性,這些屬性對於這個 item 來講是惟一的:pictureUrl 和 userName。二者都是沒有初始值的存儲屬性,所以咱們還須要爲這個類提供 init 方法:
class ProfileViewModelNameAndPictureItem: ProfileViewModelItem {
var type: ProfileViewModelItemType {
return .nameAndPicture
}
var sectionTitle: String {
return 「Main Info」
}
var pictureUrl: String
var userName: String
init(pictureUrl: String, userName: String) {
self.pictureUrl = pictureUrl
self.userName = userName
}
}
複製代碼
而後咱們能夠建立剩餘的4個 Model:
class ProfileViewModelAboutItem: ProfileViewModelItem {
var type: ProfileViewModelItemType {
return .about
}
var sectionTitle: String {
return 「About」
}
var about: String
init(about: String) {
self.about = about
}
}
class ProfileViewModelEmailItem: ProfileViewModelItem {
var type: ProfileViewModelItemType {
return .email
}
var sectionTitle: String {
return 「Email」
}
var email: String
init(email: String) {
self.email = email
}
}
class ProfileViewModelAttributeItem: ProfileViewModelItem {
var type: ProfileViewModelItemType {
return .attribute
}
var sectionTitle: String {
return 「Attributes」
}
var rowCount: Int {
return attributes.count
}
var attributes: [Attribute]
init(attributes: [Attribute]) {
self.attributes = attributes
}
}
class ProfileViewModeFriendsItem: ProfileViewModelItem {
var type: ProfileViewModelItemType {
return .friend
}
var sectionTitle: String {
return 「Friends」
}
var rowCount: Int {
return friends.count
}
var friends: [Friend]
init(friends: [Friend]) {
self.friends = friends
}
}
複製代碼
對於 ProfileViewModeAttributeItem 和 ProfileViewModeFriendsItem,咱們可能會有多個 Cell,因此 RowCount 將是相應的 Attributes 數量和 Friends 數量。
這就是數據項所需的所有內容。最後一步是建立 ViewModel 類。這個類能夠被任何 ViewController 使用,這也是MVVM結構背後的關鍵思想之一:你的 ViewModel 對 View 一無所知,但它提供了 View 可能須要的全部數據。
_ViewModel_擁有的惟一屬性是 item 數組,它對應着 UITableView 包含的 section 數組:
class ProfileViewModel: NSObject {
var items = [ProfileViewModelItem]()
}
複製代碼
要初始化 ViewModel,咱們將使用 Profile Model。首先,咱們嘗試將 .json 文件解析爲 Data:
class ProfileViewModel: NSObject {
var items = [ProfileViewModelItem]()
override init(profile: Profile) {
super.init()
guard let data = dataFromFile("ServerData"), let profile = Profile(data: data) else {
return
}
// initialization code will go here
}
}
複製代碼
下面是最有趣的部分:基於 Model,咱們將配置須要顯示的 ViewModel。
class ProfileViewModel: NSObject {
var items = [ProfileViewModelItem]()
override init() {
super.init()
guard let data = dataFromFile("ServerData"), let profile = Profile(data: data) else {
return
}
if let name = profile.fullName, let pictureUrl = profile.pictureUrl {
let nameAndPictureItem = ProfileViewModelNamePictureItem(name: name, pictureUrl: pictureUrl)
items.append(nameAndPictureItem)
}
if let about = profile.about {
let aboutItem = ProfileViewModelAboutItem(about: about)
items.append(aboutItem)
}
if let email = profile.email {
let dobItem = ProfileViewModelEmailItem(email: email)
items.append(dobItem)
}
let attributes = profile.profileAttributes
// we only need attributes item if attributes not empty
if !attributes.isEmpty {
let attributesItem = ProfileViewModeAttributeItem(attributes: attributes)
items.append(attributesItem)
}
let friends = profile.friends
// we only need friends item if friends not empty
if !profile.friends.isEmpty {
let friendsItem = ProfileViewModeFriendsItem(friends: friends)
items.append(friendsItem)
}
}
}
複製代碼
如今,若是要從新排序、添加或刪除 item,只需修改此 ViewModel 的 item 數組便可。很清楚,是吧?
接下來,咱們將 UITableViewDataSource 添加到 ModelView:
extension ViewModel: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items[section].rowCount
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// we will configure the cells here
}
}
複製代碼
讓咱們回到 ViewController 中,開始 TableView 的準備。
首先,咱們建立存儲屬性 ProfileViewModel 並初始化它。在實際項目中,你必須先請求數據,將數據提供給 ViewModel,而後在數據更新時從新加載 TableView(在這裏查看在 iOS 應用程序中傳遞數據的方法)。
接下來,讓咱們來配置 tableViewDataSource:
override func viewDidLoad() {
super.viewDidLoad()
tableView?.dataSource = viewModel
}
複製代碼
如今咱們能夠開始構建 UI 了。咱們須要建立五種不一樣類型的 Cell,每種 Cell 對應一種 ViewModelItems。如何建立 Cell 並非本教程中所須要介紹的內容,你能夠建立本身的 Cell 類、樣式和佈局。做爲參考,我將向你展現一些簡單示例:
若是你對建立 Cell 須要一些幫助,或者想要一些提示,能夠查看我以前關於 tableViewCells 的某個 教程 。
每一個 Cell 都應該具備 ProfileViewModelItem 類型的 item 屬性,咱們將使用它來構建 Cell UI:
// this assumes you already have all the cell subviews: labels, imagesViews, etc
class NameAndPictureCell: UITableViewCell {
var item: ProfileViewModelItem? {
didSet {
// cast the ProfileViewModelItem to appropriate item type
guard let item = item as? ProfileViewModelNamePictureItem else {
return
}
nameLabel?.text = item.name
pictureImageView?.image = UIImage(named: item.pictureUrl)
}
}
}
class AboutCell: UITableViewCell {
var item: ProfileViewModelItem? {
didSet {
guard let item = item as? ProfileViewModelAboutItem else {
return
}
aboutLabel?.text = item.about
}
}
}
class EmailCell: UITableViewCell {
var item: ProfileViewModelItem? {
didSet {
guard let item = item as? ProfileViewModelEmailItem else {
return
}
emailLabel?.text = item.email
}
}
}
class FriendCell: UITableViewCell {
var item: Friend? {
didSet {
guard let item = item else {
return
}
if let pictureUrl = item.pictureUrl {
pictureImageView?.image = UIImage(named: pictureUrl)
}
nameLabel?.text = item.name
}
}
}
var item: Attribute? {
didSet {
titleLabel?.text = item?.key
valueLabel?.text = item?.value
}
}
複製代碼
大家可能會提一個合理的問題:爲何咱們不爲 ProfileViewModelAboutItem 和 ProfileViewModelEmailItem 建立同一個的 Cell,他們都只有一個 label?答案是能夠這樣子作,咱們可使用一個的 Cell。但本教程的目的是向你展現如何使用不一樣類型的 Cell。
若是你想將它們用做 reusableCells,不要忘記註冊 Cell:UITableView 提供註冊 Cell class 和 nib 文件的方法,這取決於你建立 Cell 的方式。
如今是時候在 TableView 中使用 Cell 了。一樣,ViewModel 將以一種很是簡單的方式處理它:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = items[indexPath.section]
switch item.type {
case .nameAndPicture:
if let cell = tableView.dequeueReusableCell(withIdentifier: NamePictureCell.identifier, for: indexPath) as? NamePictureCell {
cell.item = item
return cell
}
case .about:
if let cell = tableView.dequeueReusableCell(withIdentifier: AboutCell.identifier, for: indexPath) as? AboutCell {
cell.item = item
return cell
}
case .email:
if let cell = tableView.dequeueReusableCell(withIdentifier: EmailCell.identifier, for: indexPath) as? EmailCell {
cell.item = item
return cell
}
case .friend:
if let cell = tableView.dequeueReusableCell(withIdentifier: FriendCell.identifier, for: indexPath) as? FriendCell {
cell.item = friends[indexPath.row]
return cell
}
case .attribute:
if let cell = tableView.dequeueReusableCell(withIdentifier: AttributeCell.identifier, for: indexPath) as? AttributeCell {
cell.item = attributes[indexPath.row]
return cell
}
}
// return the default cell if none of above succeed
return UITableViewCell()
}
你可使用相同的結構來構建 didSelectRowAt 代理方法:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch items[indexPath.section].type {
// do appropriate action for each type
}
}
複製代碼
最後,配置 headerView:
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return items[section].sectionTitle
}
複製代碼
構建運行你的項目並享受動態表視圖!
要測試該方法的靈活性,你能夠修改 JSON 文件:添加或刪除一些 friends 數據,或徹底刪除一些數據(只是不要破壞 JSON 結構,否則,你就沒法看到任何數據)。當你從新構建項目時,tableView 將以其應有的方式查找和工做,而無需任何代碼修改。 若是要更改 Model 自己,你只需修改 ViewModel 和 ViewController:添加新屬性,或重構其整個結構。固然那就要另當別論了。
在這裏,你能夠查看完整的項目:
謝謝你的閱讀!若是你有任何問題或建議 - 請隨意提問!
在下一篇文章中,咱們將升級現有項目,爲這些 section 添加一個良好的摺疊/展開效果。
更新:在 此處 查看如何在不使用 ReloadData 方法的狀況下動態更新此 tableView。
我同時也爲美國運通工程博客寫做。在 AmericanExpress.io 查看個人其餘做品和我那些才華橫溢的同事的做品。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。