本文转自镜画者的博客
每次写 TableView 都是又爱又恨,代码感觉总是很像,但细节不同又借鉴不了。究其原因就是代码没有真正规范和模块化。在参考了几篇文章后,我总结了一个范式,可以很大程度上对 TableView 的编写做到规范化。本文不仅是对 TableView 的总结,同时也是对协议、枚举等类型的使用探讨。
参考文章如下:
本文重点从数据模型角度进行优化,TableView 的数据模型分为三种情况:动态类型 cell(Dynamic Prototypes)、静态类型 cell(Static Cells)、动静态混合类型。
先看看优化后的总体模型结构图:
优化的关键在于配合协议和枚举对模型的合理设计,下面我们先看看动态类型:
动态类型 TableView 优化
我们接着上次的示例工程进行改写(上次教程 参见这里 ,Github 示例工程地址)
在上次的示例工程中我们有一个展示书单的简单列表,下面我们将添加以下功能:
1.从豆瓣获取我收藏的书籍
2.列表分为 3 个 Sectinon 展示:想看、在看、已看的书
3.在列表中交替展示两种类型的 Cell(即异构类型数据模型):书籍名称、书籍评分
4.书籍评分的详情页中,将包含动静态混合数据。
最终效果如下:
现在开始编码环节:功能1、2
功能 1 需要我们发起网络请求,并解析返回数据为指定模型。这里我们使用 URLSession 发送请求,我们先添加两个协议:
NetworkProtocol.swift:在 send 方法中我们使用泛型约束,这样比直接使用 Request 协议作为参数类型更高效。
/// 网络请求发送协议 protocol Client { var host: String { get } func send(_ r: T, handler: @escaping (Data?) -> Void) } /// 网络请求内容协议 protocol Request { var path: String { get } var method: HTTPMethod { get } var parameter: [String: Any] { get } }
添加两个枚举类型:
Enums.swift :
/// Http 请求方法 public enum HTTPMethod: String { case GET case POST } /// 主机类型 /// /// - doubanAPI: 豆瓣 API enum HostType: String { case doubanAPI = "https://api.douban.com" }
再添加对应协议的请求模型:
URLSessionClient.swift:在这里我们不解析返回的 Data 类型数据,后面交给数据模型来完成。
/// 网络客户端模型 struct URLSessionClient: Client { let host: String func send(_ requestInfo: T, handler: @escaping (Data?) -> Void) { let url = URL(string: host.appending(requestInfo.path))! var request = URLRequest(url: url) request.httpMethod = requestInfo.method.rawValue let task = URLSession.shared.dataTask(with: request) { data, _, error in if let data = data { DispatchQueue.main.async { handler(data) } } else { DispatchQueue.main.async { handler(nil) } } } task.resume() } }
BookRequest.swift:在这里我们将请求得到的 Data 类型数据通过 Swift 4.0 提供的 JSONDecoder 进行解析,这比以前解析 json 的方式优雅太多了,因此接下来我们的数据模型都将要支持 Codable 协议,以使用此便利。代码中的 BookCollections 数据模型将在后面创建。
/// 书籍查询的网络请求模型 struct BookRequest: Request { let userName: String // 用户名 let status: String // 阅读状态:想读:wish 在读:reading 读过:read let start: Int // 起始编号 let count: Int // 每次查询最大数量 var path: String { return "/v2/book/user//(userName)/collections?status=/(status)&start=/(start)&count=/(count)" } let method: HTTPMethod = .GET let parameter: [String: Any] = [:] func send(handler: @escaping (BookCollections?) -> Void) { URLSessionClient(host: HostType.doubanAPI.rawValue).send(self) { data in guard let data = data else { return } if let bookCollections = try? JSONDecoder().decode(BookCollections.self, from: data) { handler(bookCollections) } else { handler(nil) print("JSON parse failed") } } } }
现在我们来创建解析后的数据模型,以下 4 个数据模型与豆瓣 API 返回的 Json 数据结构是一致的(只保留需要的属性),因此只要原样照抄即可,注意这几个结构是逐渐往下嵌套的,与 API 的嵌套结构一致,这是 Codable 协议中最美好的地方。
DataModel.swift:
typealias DataModel = BookCollections struct BookCollections: Codable { var total: Int = 0 var collections: [MyBook] = [] } struct MyBook: Codable { var status: String = "" var book: Book } struct Book: Codable { var title: String = "" var subtitle: String = "" var author: [String] = [] var publisher: String = "" var isbn13: String? var price: String = "" var pubdate: String = "" var summary: String = "" var rating: BookRating var id: String = "" } struct BookRating: Codable { var average: String = "" var numRaters: Int = 0 }
以上几个模型也可以称为 API 模型,真正应用到表格中还需要做转换,为了保持尽量解耦,我们这里设计模型时尽量保持作用分离。接下来我们设计的数据模型是与表格的需求一一对应的,但是为了达到这样的对应,还需要一个转换层,这里我们使用 ViewModel 来承担模型转换的责任。
还是先从协议定义开始,这一组协议定义的是表格最主要的数据需求,即所有 cell 显示的数据,和 section 显示的信息。
TableDataModelProtocol.swift:
/// TableView 动态类型数据模型协议。 /// 包含所有 Cell 数据的二维数组(第一层对应 section,第二层对应 section 下的 cell), /// 以及 Section 数据数组 protocol TableViewDynamicCellDataModel { associatedtype TableDataModelType: CellModelType associatedtype SectionItem: TableViewSection var dynamicTableDataModel: [[TableDataModelType]] { get set } var sectionsDataModel: [SectionItem] { get set } } /// TableView section 信息结构体模型协议,包含 section 标题信息等。 protocol TableViewSection { var headerTitle: String? { get } var footerTitle: String? { get } } /// Cell 统一数据模型协议 protocol CellModelType { // 此为包装协议,便于在其他协议中使用,可以为空。 }
CellDataModelProtocol.swift:
/// 书籍列表中的书名类 Cell 数据协议 protocol BookInfoCellProtocol { var identifier: CellIdentifierType { get } var title: String { get } var authors: String { get } var publisher: String { get } var isbn13: String { get } var price: String { get } var pubdate: String { get } var summary: String { get } }
为了让 Cell 支持多种数据类型,我们使用枚举 BookListCellModelType 将异构类型变为同构类型。
对于将异构变为同构,除了枚举还可以使用协议,将多个类型遵从同一个协议,但是使用时为了区分不同类型代码还是不够优雅。枚举的好处是使用 switch 语句时可以利用编译器检查,但是枚举最大的缺点就是提取值时有点繁琐,后面将会看到。
然而最不建议的就是使用 Any、AnyObject。
Enums.swift:CellModelType 是包装协议,协议本身是空的,只是为了涵盖所有的 Cell 数据类型枚举。
/// 书籍列表 Cell 使用的数据模型类型 /// /// - bookInfo: 图书基本信息 enum BookListCellModelType: CellModelType { case bookInfo(BookInfoCellModel) // 后续将方便扩展多个数据模型 }
接下里我们完成协议要求的数据模型:
CellModel.swift:书籍列表中的 Cell 由于是可复用的,因此我们需要把 identifier 通过枚举的形式标明。
/// 书籍列表的书籍名称类 cell 的数据结构体 struct BookInfoCellModel: BookInfoCellProtocol { var identifier = CellIdentifierType.bookInfoCell var title: String = "" var authors: String = "" var publisher: String = "" var isbn13: String = "" var price: String = "" var pubdate: String = "" var summary: String = "" } /// 列举表格中包含的所有动态 cell 标识符 public enum CellIdentifierType: String { case bookInfoCell }
SectionModel.swift:这里的 cellType、cellCount 是为静态表格预留的。
/// TableView 中的 section 数据结构体 struct SectionModel: TableViewSection { var headerTitle: String? var footerTitle: String? var cellType: CellType var cellCount: Int init(headerTitle: String?, footerTitle: String?, cellType: CellType = .dynamicCell, cellCount: Int = 0) { self.headerTitle = headerTitle self.footerTitle = footerTitle self.cellType = cellType self.cellCount = cellCount } } /// cell 类型 /// /// - staticCell: 静态 /// - dynamicCell: 动态 public enum CellType: String { case staticCell case dynamicCell }
数据模型有了,接下来要实现 VM 了,VM 要做的主要事情就是转换,将 API 模型 -> 表格数据模型。
TableViewViewModel.swift:DataModel 是 API 解析后的模型,[[BookListCellModelType]] 是表格需要的数据模型,使用二维数组的形式是因为在 DataSource 代理方法中使用起来非常方便直接。
getTableDataModel 方法是用来进行数据结构包装的。getBookInfo 方法是真正进行数据细节上的转换的,如果有字段映射的变动在这里修改就可以了。
struct TableViewViewModel { /// 构造表格统一的数据模型 /// /// - Parameter model: 原始数据模型 /// - Returns: 表格数据模型 static func getTableDataModel(model: DataModel) -> [[BookListCellModelType]] { var bookWishToRead: [BookListCellModelType] = [] var bookReading: [BookListCellModelType] = [] var bookRead: [BookListCellModelType] = [] for myBook in model.collections { guard let status = BookReadingStatus(rawValue: myBook.status) else { return [] } let bookInfo = getBookInfo(model: myBook.book) switch status { case .wish: bookWishToRead.append(bookInfo) case .reading: bookReading.append(bookInfo) case .read: bookRead.append(bookInfo) case .all: break } } return [bookWishToRead, bookReading, bookRead] } /// 获取 BookInfoCellModel 数据模型 /// /// - Parameter model: 原始数据子模型 /// - Returns: 统一的 cell 数据模型 static func getBookInfo(model: Book) -> BookListCellModelType { var cellModel = BookInfoCellModel() cellModel.title = model.title cellModel.authors = model.author.reduce("", { $0 == "" ? $1 : $0 + "、" + $1 }) cellModel.publisher = model.publisher cellModel.isbn13 = model.isbn13 ?? "" cellModel.price = model.price cellModel.pubdate = model.pubdate cellModel.summary = model.summary return BookListCellModelType.bookInfo(cellModel) } } /// 书籍阅读状态 public enum BookReadingStatus: String { case wish case reading case read case all = "" }
好了,有了上面的模型,我们就可以调用 API 获取数据了。
MainTableViewController.swift:loadData() 方法加载完数据后会自动刷新表格
/// 数据源对象 var dynamicTableDataModel: [[BookListCellModelType]] = [] { didSet { tableView.reloadData() } } override func viewDidLoad() { super.viewDidLoad() loadData() // 加载数据 // ... } /// 加载初始数据 func loadData() { let request = BookRequest(userName: "pmtao", status: "", start: 0, count: 40) request.send { data in guard let dataModel = data else { return } let tableDataModel = TableViewViewModel.getTableDataModel(model: dataModel) self.dynamicTableDataModel = tableDataModel } }
现在写起数据源代理方法来别提多简单:
MainTableViewController+DataSource.swift:下面代码几乎可以无痛移植到另一个 TabelView 中,在代码注释的地方改个名即可。这样的 DataSource 是不是清爽多了,这样的代码写一遍基本就可以不用理了。其中有个 configureCell 方法,我们统统移到自定义 Cell 类文件中实现,让 Cell 完成自己的配置,喂给 Cell 的数据都是不用转换的,多亏了协议的功劳(BookInfoCellProtocol)。
override func numberOfSections(in tableView: UITableView) -> Int { return dynamicTableDataModel.count } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return dynamicTableDataModel[section].count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let section = indexPath.section let row = indexPath.row let model = dynamicTableDataModel[section][row] switch model { case let .bookInfo(bookInfo): // bookInfo 移植改名 let identifier = bookInfo.identifier.rawValue // bookInfo 移植改名 let cell = tableView.dequeueReusableCell( withIdentifier: identifier, for: indexPath) as! BookInfoCell // BookInfoCell 移植改名 cell.configureCell(model: bookInfo) // bookInfo 移植改名 return cell } }
接下来实现功能3
展示两种类型的 Cell(即异构类型数据模型):书籍名称、书籍评分,扩展前面的模型即可:
Enums.swift:增加评分信息的 Cell 数据类型。
/// 书籍列表 Cell 使用的数据模型类型 /// /// - bookInfo: 图书基本信息 /// - bookRating: 图书评分信息 enum BookListCellModelType: CellModelType { case bookInfo(BookInfoCellModel) case bookRating(BookRatingCellModel) }
CellDataModelProtocol.swift:增加评分信息的 Cell 数据协议。
/// 书籍列表中的评分类 Cell 数据协议 protocol BookRatingCellProtocol { var identifier: CellIdentifierType { get } var average: String { get } var numRaters: String { get } var id: String { get } var title: String { get } }
CellModel.swift:增加评分类 cell 的数据结构体
/// 书籍列表的书籍评分类 cell 的数据结构体 struct BookRatingCellModel: BookRatingCellProtocol { var identifier = CellIdentifierType.bookRatingCell var average: String = "" var numRaters: String = "" var id: String = "" var title: String = "" }
TableViewViewModel.swift:VM 中再增加下数据模型转换方法:
static func getTableDataModel(model: DataModel) -> [[BookListCellModelType]] { ... let bookRating = getBookRating(model: myBook.book) switch status { case .wish: bookWishToRead.append(bookInfo) bookWishToRead.append(bookRating) // 增加的数据类型 case .reading: bookReading.append(bookInfo) bookReading.append(bookRating) // 增加的数据类型 case .read: bookRead.append(bookInfo) bookRead.append(bookRating) // 增加的数据类型 case .all: break } ... } /// 获取 BookRatingCellModel 数据模型 /// /// - Parameter model: 原始数据子模型 /// - Returns: 统一的 cell 数据模型 static func getBookRating(model: Book) -> BookListCellModelType { var cellModel = BookRatingCellModel() cellModel.average = "评分:" + model.rating.average cellModel.numRaters = "评价人数:" + String(model.rating.numRaters) cellModel.id = model.id cellModel.title = model.title return BookListCellModelType.bookRating(cellModel) }
最后一步,DataSource 微调一下:
MainTableViewController+DataSource.swift:添加一个 case 即可,编译器还会自动提示你,使用枚举封装异构数据类型是不是很爽.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let section = indexPath.section let row = indexPath.row let model = dynamicTableDataModel[section][row] switch model { case let .bookInfo(bookInfo): let identifier = bookInfo.identifier.rawValue let cell = tableView.dequeueReusableCell( withIdentifier: identifier, for: indexPath) as! BookInfoCell cell.configureCell(model: bookInfo) return cell // 新增数据类型部分: case let .bookRating(bookRating): let identifier = bookRating.identifier.rawValue let cell = tableView.dequeueReusableCell( withIdentifier: identifier, for: indexPath) as! BookRatingCell cell.configureCell(model: bookRating) return cell } }
静态类型 TableView 优化
我们改造一下书籍详情页
详情页是纯静态类型表格,用类似的方式封装协议和模型,更是简洁到无以复加。
CellDataModelProtocol.swift:增加详情页 Cell 的数据模型协议
/// 图书详情类 Cell 数据协议 protocol BookDetailCellProtocol { var title: String { get } var authors: String { get } var publisher: String { get } }
CellModel.swift:增加详情页 Cell 的数据模型
/// 书籍详情类 cell 的数据结构体 struct BookDetailCellModel: BookDetailCellProtocol { var title: String = "" var authors: String = "" var publisher: String = "" }
TableViewViewModel.swift:增加书籍详情的模型转换
/// 获取 BookDetailCellModel 数据模型 /// /// - Parameter model: BookInfoCellModel 数据模型 /// - Returns: BookDetailCellModel 数据模型 static func getBookDetail(model: BookInfoCellModel) -> BookDetailCellModel { var cellModel = BookDetailCellModel() cellModel.title = model.title cellModel.authors = model.authors cellModel.publisher = model.publisher return cellModel }
TableDataModelProtocol.swift:增加静态类型 TableView 数据模型协议
/// TableView 静态类型数据模型协议。 /// 包含 Cell 结构体数据、 Section 数据数组 protocol TableViewStaticCellDataModel { associatedtype StaticCellDataModel associatedtype SectionItem: TableViewSection var staticTableDataModel: StaticCellDataModel { get set } var sectionsDataModel: [SectionItem] { get set } }
DetailTableViewController.swift:最后集成一下,一共 40 行,是不是很简洁.
import UIKit class DetailTableViewController: UITableViewController, TableViewStaticCellDataModel { // MARK: 1.--@IBOutlet属性定义----------- @IBOutlet weak var nameLabel: UILabel! @IBOutlet weak var authorsLabel: UILabel! @IBOutlet weak var publisherLabel: UILabel! // MARK: 2.--实例属性定义---------------- var staticTableDataModel = BookDetailCellModel() var sectionsDataModel: [SectionModel] = [] // MARK: 3.--视图生命周期---------------- override func viewDidLoad() { super.viewDidLoad() setSectionDataModel() // 设置 section 数据模型 configureCell(model: self.staticTableDataModel) // 配置 Cell 显示内容 } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } // MARK: 4.--处理主逻辑----------------- /// 设置 section 数据模型 func setSectionDataModel() { sectionsDataModel = [SectionModel(headerTitle: nil, footerTitle: nil, cellCount: 3)] } /// 配置静态 Cell 显示内容 func configureCell(model: T) { nameLabel?.text = model.title authorsLabel?.text = model.authors publisherLabel?.text = model.publisher } // MARK: 5.--数据源方法------------------ override func numberOfSections(in tableView: UITableView) -> Int { return sectionsDataModel.count } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return sectionsDataModel[section].cellCount } }
动静态混合类型 TableView 优化
最后一个骨头:功能4
功能 4 要实现一个动静态混合的表格,这个话题也是 TabelView 改造中很常见的一个话题。我尝试了几种方案,总想用点黑科技偷点懒,实验完发现还是得用常规思路。现总结如下:
先用静态表格进行设计,动态数据部分只需预留一个 Cell 即可(纯占位,无需做任何设置)。
新建 UITableViewCell 子类和配套的 xib 文件,添加 Identifier,这个用来复用动态 cell 部分。
在数据源中判断 cell 类型(静态、动态),并返回相应的 cell。
开动吧。
Enums.swift:增加点类型
/// 列举表格中包含的所有动态 cell 标识符 public enum CellIdentifierType: String { case bookInfoCell case bookRatingCell case bookReviewTitleCell } /// Cell nib 文件类型 public enum CellNibType: String { case BookReviewListTableViewCell } /// 书籍评论列表页的评分项 Cell 使用的数据模型类型 enum BookReviewCellModelType: CellModelType { case bookReviewList(BookReviewListCellModel) }
TableDataModelProtocol.swift:创建一个混合版数据模型协议,其实就是合并了动态、静态的数据类型
/// TableView 动态、静态混合类型数据模型协议。 /// 包含动态 Cell 二维数组模型、静态 Cell 结构体数据、Section 数据数组、动态 Cell 的复用信息。 protocol TableViewMixedCellDataModel { associatedtype TableDataModelType: CellModelType associatedtype StaticCellDataModel associatedtype SectionItem: TableViewSection var dynamicTableDataModel: [[TableDataModelType]] { get set } var staticTableDataModel: StaticCellDataModel { get set } var sectionsDataModel: [SectionItem] { get set } var cellNibs: [(CellNibType, CellIdentifierType)] { get set } }
CellDataModelProtocol.swift:Cell 的数据模型协议分为动态、静态两部分。
/// 图书评论摘要列表数据协议 protocol BookReviewListCellProtocol { var identifier: CellIdentifierType { get } var title: String { get } var rate: String { get } var link: String { get } } /// 图书评论标题数据协议 protocol BookReviewHeadCellProtocol { var title: String { get } var rate: String { get } }
BookReviewRequest.swift:评论数据需要单独发起网络请求,新增一种请求模型即可。
struct BookReviewRequest: Request { let bookID: String // 书籍 ID let start: Int // 起始编号 let count: Int // 每次查询最大数量 var path: String { return "/v2/book//(bookID)/reviews?start=/(start)&count=/(count)" } let method: HTTPMethod = .GET let parameter: [String: Any] = [:] func send(handler: @escaping (BookReview?) -> Void) { URLSessionClient(host: HostType.doubanAPI.rawValue).send(self) { data in guard let data = data else { return } if let bookReviews = try? JSONDecoder().decode(BookReview.self, from: data) { handler(bookReviews) } else { handler(nil) print("JSON parse failed") } } } }
DataModel.swift:API 模型也相应新增。
struct BookReview: Codable { var reviews: [Review] = [] struct Review: Codable { var rating: Score var title: String = "" var alt: String = "" // 评论页链接 } struct Score: Codable { var value: String = "" } }
CellModel.swift:实现 cell 数据协议所需的模型,也按动态、静态区分开。
/// 书籍评论详情的摘要列表类 cell 的数据结构体 struct BookReviewListCellModel: BookReviewListCellProtocol { var identifier = CellIdentifierType.bookReviewTitleCell var title: String = "" var rate: String = "" var link: String = "" } /// 书籍评论详情的评论标题类 cell 的数据结构体 struct BookReviewHeadCellModel: BookReviewHeadCellProtocol { var id: String = "" var title: String = "" var rate: String = "" }
TableViewViewModel.swift:VM 中再把 API 模型转换到 cell 数据模型。
/// 获取 BookReviewListCellModel 数据模型 /// /// - Parameter model: BookReview 数据模型 /// - Returns: 书籍评论页需要的评论列表模型 static func getBookReviewList(model: BookReview) -> [[BookReviewCellModelType]] { var cellModel: [BookReviewCellModelType] = [] for review in model.reviews { var bookReviewListCellModel = BookReviewListCellModel() bookReviewListCellModel.title = review.title bookReviewListCellModel.rate = "评分:" + review.rating.value bookReviewListCellModel.link = review.alt // 转换为 enum 类型 let model = BookReviewCellModelType.bookReviewList(bookReviewListCellModel) cellModel.append(model) } return [[], cellModel] } /// 获取 BookReviewHeadCellModel 数据模型 /// /// - Parameter model: Book 数据模型 /// - Returns: 书籍评论页需要的标题信息 static func getBookReviewHead(model: BookRatingCellModel) -> BookReviewHeadCellModel { var cellModel = BookReviewHeadCellModel() cellModel.id = model.id cellModel.title = model.title cellModel.rate = model.average return cellModel }
ReviewTableViewController.swift:最后的集成,静态部分的 Cell 直接把要设置的控件建立 IBOutlet,用数据模型映射一下就好。
class ReviewTableViewController: UITableViewController, TableViewMixedCellDataModel { // MARK: 1.--@IBOutlet属性定义----------- @IBOutlet weak var bookNameLabel: UILabel! @IBOutlet weak var rateLabel: UILabel! // MARK: 2.--实例属性定义---------------- /// 数据源对象 var dynamicTableDataModel: [[BookReviewCellModelType]] = [] { didSet { if shouldReloadTable { setSectionDataModel() tableView.reloadData() } } } var staticTableDataModel = BookReviewHeadCellModel() var sectionsDataModel: [SectionModel] = [] var cellNibs: [(CellNibType, CellIdentifierType)] = [(.BookReviewListTableViewCell, .bookReviewTitleCell)] /// 有数据更新时是否允许刷新表格 var shouldReloadTable: Bool = false
再进行一些初始化设置,注意:在 viewDidLoad 方法中就已经可以对静态 cell 通过 IBOutlet 进行配置了。
// MARK: 3.--视图生命周期---------------- override func viewDidLoad() { super.viewDidLoad() loadData() // 加载数据 setSectionDataModel() // 设置 section 数据模型 configureStaticCell(model: staticTableDataModel) // 配置 Cell 显示内容 setupView() // 视图初始化 } // MARK: 4.--处理主逻辑----------------- /// 加载初始数据 func loadData() { let request = BookReviewRequest(bookID: staticTableDataModel.id, start: 0, count: 3) request.send { data in guard let dataModel = data else { return } let tableDataModel = TableViewViewModel.getBookReviewList(model: dataModel) self.shouldReloadTable = true self.dynamicTableDataModel = tableDataModel } } /// 设置 section 数据模型 func setSectionDataModel() { let section1 = SectionModel( headerTitle: "书籍", footerTitle: nil, cellType: .staticCell, cellCount: 2) var section2CellCount = 0 if dynamicTableDataModel.count > 0 { section2CellCount = dynamicTableDataModel[1].count } let section2 = SectionModel( headerTitle: "精选评论", footerTitle: nil, cellType: .dynamicCell, cellCount: section2CellCount) sectionsDataModel = [section1, section2] } /// 配置静态 Cell 显示内容 func configureStaticCell(model: T) { bookNameLabel?.text = model.title rateLabel?.text = model.rate } /// 视图初始化相关设置 func setupView() { // 注册 cell nib 文件 for (nib, identifier) in cellNibs { let nib = UINib(nibName: nib.rawValue, bundle: nil) tableView.register(nib, forCellReuseIdentifier: identifier.rawValue) } }
关键的数据源方法来了:
// MARK: 8.--数据源方法------------------ override func numberOfSections(in tableView: UITableView) -> Int { return sectionsDataModel.count } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return sectionsDataModel[section].cellCount }
我们在 sectionsDataModel 的数据中就已经包含了 Cell 的动静态类型,因此可以直接拿来判断。静态类型的 Cell 通过 super 属性即可直接获取, super 其实就是控制器对象本身,从中获取的 Cell 是从 StoryBoard 中初始化过的实例,这样获取可以避免 cellForRowAt 再调用自身方法造成死循环。动态类型 Cell 直接调用 dequeueReusableCell 方法即可,注意要带 for: indexPath 参数的那个。
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let section = indexPath.section let row = indexPath.row if sectionsDataModel[section].cellType == .staticCell { let cell = super.tableView(tableView, cellForRowAt: indexPath) return cell } else { let model = dynamicTableDataModel[section][row] switch model { case let .bookReviewList(bookReviewList): let identifier = bookReviewList.identifier.rawValue let cell = tableView.dequeueReusableCell( withIdentifier: identifier, for: indexPath) as! BookReviewListTableViewCell cell.configureCell(model: bookReviewList) return cell } } }
视图代理方法中还有一些要补充的,这些方法是由于套用静态 TableView 来实现动态 cell 效果带来的副作用,照着写就行:
// MARK: 9.--视图代理方法---------------- // 复用静态 cell 时要使用这个代理方法 override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { let section = indexPath.section if sectionsDataModel[section].cellType == .staticCell { return super.tableView(tableView, heightForRowAt: indexPath) } else { let prototypeCellIndexPath = IndexPath(row: 0, section: indexPath.section) return super.tableView(tableView, heightForRowAt: prototypeCellIndexPath) } } // 复用静态 cell 时要使用这个代理方法 override func tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int { let section = indexPath.section if sectionsDataModel[section].cellType == .staticCell { return super.tableView(tableView, indentationLevelForRowAt: indexPath) } else { // 将 storyBoard 中绘制的原型 cell 的 indentationLevel 赋予其他 cell let prototypeCellIndexPath = IndexPath(row: 0, section: indexPath.section) return super.tableView(tableView, indentationLevelForRowAt: prototypeCellIndexPath) } } // 设置分区标题 override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return sectionsDataModel[section].headerTitle }
优化终于完成了!
真不容易,看到这里是不是有点晕,其实总结一下,拆分并实现以下模块,编写 TableView 就可以做到很优雅了,以后基本就可以全套套用了:
定义网络请求协议
定义表格数据模型协议
定义网络请求模型
定义 API 数据模型
定义表格数据模型
定义 Cell 数据需求模型
定义视图模型
定义 UITableViewCell 子类
完整工程已上传 Github 工程地址
欢迎访问 我的个人网站 ,阅读更多文章。