Apply multiple MVCs pattern to solve dynamic cells Table View

Tung Vu Duc 🇻🇳
7 min readJun 7, 2020

Bên cạnh UICollectionView, UITableView có lẽ là loại View được các iOS Developers sử dụng nhiều nhất. UITableView giúp chúng ta dễ dàng xây dựng một giao diện dạng list, theo chiều dọc.

Ngoài những Table Views đơn giản chỉ có duy nhất một cell type, trên thực tế đi làm bạn sẽ gặp hầu hết là các Table Views phức tạp hiển thị nhiều loại thông tin khác nhau và đòi hỏi phải sử dụng nhiều cell types còn được gọi là dynamic cells table view.

Thành thật mà nói nếu làm theo cách “thông thường” (mình sẽ đề cập cách này ở phần tiếp theo) thì việc xây dựng một dynamic cells table view cũng không quá khó khăn. Nhưng trong bài viết này, hãy cùng mình tiến hoá từ “thông thường” thành “pro” để code clean hơn, dễ dàng mở rộng, thay đổi hơn.

Trước khi đi vào nội dung chính, mình muốn shout out tới Stan Ostrovskiy, tác giả của bài viết dưới đây và chính bài viết đó cũng là cảm hứng để mình ngồi viết bài này, vì mình tin mình vẫn có thể làm tốt hơn cách mà anh ấy đã làm.

Mình cùng sẽ mượn source code của bài viết đó như là starter-project để sử dụng và bắt đầu cải tiến từ đó. Các bạn có thể tải source code trong bài viết trên hoặc tải tại đây, mình đã update syntax cho Swift 5. OK chúng ta cùng bắt đầu.

Determine the problem

Chạy starter-project, các bạn sẽ được một dynamic table view với năm sections: Main Info, About, Email, Attributes và Friends, mỗi section lại có các cells với layout khác nhau:

Cách “thông thường” mà mình nói tới ở phần đầu của bài viết chính là : xác định cell type dựa trên index của UITableView, sử dụng if-else khắp nơi

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {   
if indexPath.row == 0 {
//cell.model = models[indexPath.row]
} else if indexPath.row == 1 {
//cell.model = models[indexPath.row]
}
...
}

Nếu cần xử lý các sự kiện khác trong Table View, chúng ta lại phải phải thêm hàng loại câu lệnh if-else tương tự:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {   if indexPath.row == 0 {
// event when tap on cell at indexPath = 0
} else if indexPath.row == 1 {
// event when tap on cell at indexPath = 0
}
...
}

Như mình cũng đã nói, cách này không khó làm nhưng vấn đề của nó, đầu tiên dễ nhận ra là sự lặp lại code, thứ hai quan trọng hơn là nó quá rigid và “dễ vỡ”, mọi thay dù nhỏ trên TableView đều khiến mình phải thay đổi rất nhiều trong codebase thậm chí là viết lại toàn bộ Controller, duới đây là một số trường hợp chứng minh :

  • Nếu muốn thay đổi thứ tự của các cell => viết lại toàn bộ những đoạn if-else .
  • Nếu muốn chèn một cell vào vị trí bất kì trong Table View => viết lại toàn bộ những đoạn if-else .
  • Nếu muốn bỏ một cell nào đó => viết lại toàn bộ những đoạn if-else .

Thêm nữa, việc check indexPath.row == 0 chẳng đưa cho người đọc một chút ngữ cảnh nào, mình tin rằng những người khác hay thậm chí chính cả bạn sau một vài tháng sau nhìn lại chắc chắn cũng sẽ bối rối khi đọc lại chính những đoạn code do mình viết ra. Trong bài viết của tác giả Stan Ostrovskiy anh ấy đã sử dụng enum để cải thiện ngữ cảnh, nhưng vẫn chưa giải quyết được những vấn đề còn lại.

Mình thích gọi cách làm này là Do and Pray, pray rằng spec sẽ không thay đổi, pray rằng giao diện sẽ không đổi để bạn không phải viết lại toàn bộ logic, nhưng đáng tiếc điều đó là không thực tế vì phần mềm luôn thay đổi.

Điểm tốt trong cách làm của Stan OstrovskiyViewController (Controller) rất clean (chỉ hơn 30 dòng code), nó chỉ chịu trách nhiệm configure UITableView, nhưng ProfileViewModel (data source) đang nhận quá nhiều responsibilities nên chúng ta sẽ chỉ cần tập trung improve ProfileViewModel .

ProfileViewModel’s responsibilities

Multiple Tiny MVCs

Mở ProfileViewModel , trong method tableView(_ tableView:cellForRowAt indexPath) các bạn sẽ thấy các models (Model) đang được truyền thằng vào các cells (View) qua đó vi phạm mô hình MVC, mô hình MVC nói rằng Model và View không nên giao tiếp trực tiếp với nhau mà thông qua Controller. Vậy làm thế nào để giải quyết ?

Cách đầu tiên, các bạn có thể configure cell ngay trong hàm tableView(_ tableView:cellForRowAt indexPath) :

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {   
if indexPath.row == 0 {
//cell.imageView.image = ...
//cell.titleLabel.text = ...
} else if indexPath.row == 1 {
//configure cell type 2
}
...
}

Cách này rất phổ biến, mình cũng thấy rất nhiều, nhưng nó không phù hợp với mục đích của chúng ta là giảm responsibilities của ProfileViewModel.

Cách thứ hai, bạn có thể sử dụng các mô hình khác như MVVM, MVP,… Các này hoàn toàn ổn và tốt hơn MVC, tuy nhiên trong bài viết này mình muốn giới thiệu tới các bạn một cách thứ ba, chúng ta vẫn sẽ sử dụng MVC 😉

Cách thứ ba, áp dụng mô hình multiple MVCs. Một trong những sự lầm tưởng mình thường thấy là các bạn nghĩ rằng một screen chỉ được chứa một MVC, nhưng thực sự không phải như vậy, ở đây để giải quyết việc couple giữa Model và View thì mỗi cell hoàn toàn có thể là một tiny MVC.

tiny MVCs

Implementation

Model

Mình sửa lại các models như sau, đồng thời chuyển nó sang một file riêng (vì nó xứng đáng):

Controller

Đầu tiên tạo một protocol tên ProfileCellProtocol như sau, mục đích để các Controllers conform theo:

Sau đó, mình implement các Cell Controllers như sau:

Các bạn có thể thấy thay vì để DataSource, giờ đây mỗi Cell Controller sẽ chịu trách nhiệm tạo và configure cell bằng Model được truyền vào thông qua constructor injection.

Tiếp theo, vì chúng ta có năm sections nên mình tạo một ProfileSectionControllerProtocolProfileSectionController để chứa các Cell Controllers vừa tạo:

Parsing Data

Mình cũng muốn di chuyển phần parse dữ liệu ra khỏi ProfileViewModel vì đây rõ ràng không phải trách nhiệm của một Datasource:

Finally the ProfileViewModel

Chỉ 22 dòng code, rất gọn gàng phải không 🥳. Không còn Switch case, không còn if-else, tự do làm sao. Hãy cùng xem diagram dưới đây để xem chúng ta đã đạt được những gì:

Bằng cách chia sẻ responsibilities cho các component khác chúng ta đã tạo ra những component gọn gàng hơn, flexible hơn nhiều. Giờ đây, ProfileViewModel không còn cần phải quan tâm nó đang render cell gì, model là gì,… qua đó dễ dàng thêm Cell, xoá Cell, mà không ảnh hưởng gì tới ProfileViewModel .Chúng ta cũng đã giải quyết được vấn đề couple giữa View và Model.

Conclusion

Mặc dù bài viết của Stan Ostrovskiy rất viral, rất nhiều claps nhưng không có nghĩa rằng chúng ta không có gì để cải tiến, ở bài viết này mình đã chứng minh điều đó.

Tổng kết lại, có 2 điểm mình muốn nhấn mạnh thông qua bài viết này:

  • Đừng gắn với suy nghĩ rằng một screen chỉ có thể chứa một MVC, nó có thể chứa rất nhiều MVCs con.
  • Kể cả những bài viết rất viral trên mạng cũng không luôn luôn là silver bullet cho bạn, đừng chỉ copy and paste, hãy luôn cố gắng cải tiến nó.

Các bạn có thể tải source code của bài viết tại đây:

Và cuối cùng, nếu các bạn cảm thấy bài viết hữu ích đừng quên claps và follow mình. Cảm ơn các bạn đã đọc bài.

--

--

Tung Vu Duc 🇻🇳

Passionate about writing good software. Contact me: 📮tungvuduc2805@gmail.com