+1

Tổ chức TableView một cách hiệu quả trong iOS

Giới thiệu

TableView là một trong những component sử dụng phổ biến trong các ứng dụng iOS. Trong bài viết này ta sẽ phân tích và từng bước để tổ chức Table View một cách hiệu quả để dễ dàng bảo trì và áp dụng được nhiều yêu cầu khác nhau về dữ liệu.

Giả sử ta cần xây dựng một TableView với nhiều kiểu cell khác nhau, giống như hình dưới đây:

Với yêu cầu như sau:

  • Dữ liệu hiển thị sẽ được lấy thông qua lời gọi API
  • Không hiển thị các section không có dữ liệu

Ta sẽ chọn mô hình MVVM cho ví dụ của bài viết này. Với MVVM ta sẽ dễ dàng tách biệt View và nghiệp vụ của bài toán. Trong ví dụ này ta sẽ tạo một TableView với 3 kiểu cell khác nhau:

  1. Cell chỉ có label
  2. Cell gồm ảnh và label
  3. Cell có chức năng mở rộng

Bước 1: Tạo mới ViewController và Storyboard

Đầu tiên ta sẽ tạo một Storyboard cho dynamic TableView, và thêm TableView vào trong UIViewController.

Khi đó code của ViewController sẽ có dạng như sau:

class TableViewExampleViewController: UIViewController {
   @IBOutlet weak var tableView: UITableView?
 
   override func viewDidLoad() {
      super.viewDidLoad()
   }
}

Bước 2: Tạo protocol cho TableViewCell và TableViewCellModel

Ta sẽ tạo 2 protocol cho:

  1. Cho TableView cell
  2. Cho TableView cell ViewModel

Ta sẽ tạo protocol TableViewCellViewModelProtocol cho ViewCell model. Biến cellIdentifier sẽ được dùng để phân biệt giữa các loại cell.

protocol TableViewCellModelProtocol {
   var cellIdentifier: String { get }
}

Tiếp đó ta tạo protocol TableViewCellProtocol cho ViewCell. Protocol này có phương thức populate được dùng để lấy dữ liệu bên trong TableViewCell

protocol TableViewCellProtocol {
   func populate(with data: TableViewCellModelProtocol)
}

Bước 3: Tạo TableViewCell và TableViewCellViewModel

Ta sẽ tiến hành tạo 3 TableView cell và hiển thị dữ liệu dựa trên ViewModel.

Loại 1: TableView cell chỉ có label

ViewModel sẽ có thuộc tính sau:

  • cellIdentifier
  • title
struct TableViewCellWithLabelModel: TableViewCellModelProtocol {
   var cellIdentifier: String = "TableViewCellWithLabel"
   var title: String
   
init(title: String) {
      self.title = title
   }
}

ViewCell sẽ có dạng như sau:

class TableViewCellWithLabel: UITableViewCell {
   @IBOutlet weak var titleLabel: UILabel!
}
extension TableViewCellWithLabel: TableViewCellProtocol {
   func populate(with data: TableViewCellModelProtocol) {
      if let data = data as? TableViewCellWithLabelModel {
         titleLabel.text = data.title
      }
   }
}

Loại 2: TableViewCell gồm ảnh và label

Trong cell sẽ gồm 1 ảnh và 1 label như hình dưới.

Cell model sẽ có dạng như sau:

struct TableViewCellWithImageAndLabelModel: TableViewCellModelProtocol {
var cellIdentifier: String = "TableViewCellWithImageAndLabel"
   var imageName: String
   var title: String
init(title: String, imageName: String) {
      self.title = title
      self.imageName = imageName
   }
}

Cell View sẽ có dạng như sau:

class TableViewCellWithImageAndLabel: UITableViewCell {
   @IBOutlet weak var titleLabelView: UILabel!
   @IBOutlet weak var leadingImageView: UIImageView!
}
extension TableViewCellWithImageAndLabel: TableViewCellProtocol {
   func populate(with data: TableViewCellModelProtocol) {
      if let data = data as? TableViewCellWithImageAndLabelModel {
         titleLabelView.text = data.title
         leadingImageView.image = UIImage(named: data.imageName)
      }
   }
}

Loại 3: Cell có chức năng mở rộng

Cell model sẽ có dạng như sau:

struct TableViewExpandableCellModel: TableViewCellModelProtocol {
   var cellIdentifier: String = "TableViewExpandableCell"
   var title: String
   var description: String
   var isExpanded: Bool
init(with title: String, description: String, isExpanded: Bool =    false) {
      self.title = title
      self.description = description
      self.isExpanded = isExpanded
   }
}

Cell View sẽ có dạng như sau:

class TableViewExpandableCell: UITableViewCell {
   @IBOutlet weak var descriptionLabel: UILabel!
   @IBOutlet weak var titleLabel: UILabel!
}
extension TableViewExpandableCell: TableViewCellProtocol {
   func populate(with data: TableViewCellModelProtocol) {
      if let data = data as? TableViewExpandableCellModel {
         descriptionLabel.text = data.description
         titleLabel.text = data.title
         descriptionLabel.isHidden = !data.isExpanded
      }
   }
}

Bước 4: Tạo ViewModel cho UIViewController

ViewModel cho UIViewController sẽ gồm 2 chức năng chính:

  • Cập nhật dữ liệu cho TableView
  • Cập nhật dữ liệu cho một dòng
typealias TableViewModelOutput = (TableViewExampleViewModel.Output) -> ()
struct TableViewExampleViewModel {
var output: TableViewModelOutput?
mutating func viewModelDidLoad() {
output?(.reloadData)
   }
enum Output {
      case reloadData
      case reloadRowAt(index: Int)
   }
}

Ta sẽ sử dụng một mảng của đối tượng TableViewCellModelProtocol để lưu trữ nội dung và xử lý của từng dòng trong bảng

var items: [TableViewCellModelProtocol] = [//Set data based on your need]
var numberOfItems: Int {
   return items.count
}
func getItem(at index: Int) -> TableViewCellModelProtocol {
   return items[index]
}
mutating func didTapItem(at index: Int) {
   if var viewModel = getItem(at: index) as?TableViewExpandableCellModel {
      viewModel.isExpanded = !viewModel.isExpanded
      items[index] = viewModel
   }
 output?(.reloadRowAt(index: index))
}

Khi đó dữ liệu trong bảng sẽ được khởi tạo như sau:

mutating func viewModelDidLoad() {
   items = [
      TableViewCellWithLabelModel(title: "I am a simple title label cell"),
TableViewExpandableCellModel(with: "I am a simple expandable cell", description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry."),
TableViewCellWithImageAndLabelModel(title: "I am a simple Image Title cell", imageName: "sampleImage")
   ]
   ...
   //We can call output to reload TableView Data
}

Bước 5: Kết hợp TableView delegate và DataSource

Bước này ta sẽ tiến hình khởi tạo Delegate và DataSource cho bảng.

@IBOutlet weak var tableView: UITableView? {

didSet {
setupTableView()
}
}
private func setupTableView() {
tableView.delegate = self
tableView.dataSource = self
func registerNib() {
tableView.register(nibName: "TableViewExpandableCell")
tableView.register(nibName: "TableViewCellWithImageAndLabel")
tableView.register(nibName: "TableViewCellWithLabel")
}
registerNib()
}

Tiến đến ta sẽ viết các hàm thực thi cho các closure trong ViewModel đã được định nghĩa ở bước 4:

//Bind ViewController with ViewModel
extension TableViewExampleViewController {
   func bindViewModel() {
      viewModel.output = { output in
         switch output {
         case .reloadData:
            self.tableView.reloadData()
         case .reloadRowAt(let index):
            self.tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
         }
      }
   }
}

Cuối cùng ta sẽ thực thi các phương thức của UITableViewDelegateDataSource để trả về nội dung các cell mong muốn.

//Tableview delegate and datasource methods
extension TableViewExampleViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
   return viewModel.numberOfItems
   }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      let cellModel = viewModel.getItem(at: indexPath.row)
let cell = tableView.dequeueReusableCell(withIdentifier:    cellModel.cellIdentifier) as? TableViewCellProtocol
cell?.populate(with: cellModel)
      return cell as? UITableViewCell ?? UITableViewCell()
   }
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
      viewModel.didTapItem(at: indexPath.row)
   }
}

Khi đó table của chúng ta sẽ hoạt động như hình dưới đây

Nguồn tham khảo

https://medium.com/flawless-app-stories/clean-maintainable-and-flexible-tableview-implementation-217d6266926e


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.