转载

TableView 优化之数据模型优化

本文转自镜画者的博客

TableView 优化之数据模型优化

每次写 TableView 都是又爱又恨,代码感觉总是很像,但细节不同又借鉴不了。究其原因就是代码没有真正规范和模块化。在参考了几篇文章后,我总结了一个范式,可以很大程度上对 TableView 的编写做到规范化。本文不仅是对 TableView 的总结,同时也是对协议、枚举等类型的使用探讨。

参考文章如下:

本文重点从数据模型角度进行优化,TableView 的数据模型分为三种情况:动态类型 cell(Dynamic Prototypes)、静态类型 cell(Static Cells)、动静态混合类型。

先看看优化后的总体模型结构图:

TableView 优化之数据模型优化

优化的关键在于配合协议和枚举对模型的合理设计,下面我们先看看动态类型:

动态类型 TableView 优化

我们接着上次的示例工程进行改写(上次教程 参见这里 ,Github 示例工程地址

在上次的示例工程中我们有一个展示书单的简单列表,下面我们将添加以下功能:

1.从豆瓣获取我收藏的书籍

2.列表分为 3 个 Sectinon 展示:想看、在看、已看的书

3.在列表中交替展示两种类型的 Cell(即异构类型数据模型):书籍名称、书籍评分

4.书籍评分的详情页中,将包含动静态混合数据。

最终效果如下:

TableView 优化之数据模型优化

现在开始编码环节:功能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 工程地址

欢迎访问 我的个人网站 ,阅读更多文章。

题图:Return trip from Hana, Hawaii - Luca Bravo @unsplash

正文到此结束
Loading...