- 原文地址: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、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。