Lập trình IOS: Triển khai MVVM cho prject swift (phần 1)

Bài viết được sự cho phép của tác giả Lê Xuân Quỳnh

Hôm nay chúng ta sẽ làm 1 ứng dụng nho nhỏ, hiển thị danh sách các loại động vật, như hình ảnh:

Danh sách các loại động vật

Đầu tiên là bạn mở terminal và gõ:

Sau đó bạn cd vào thư mục code MVVMSample. Mẹo là gõ cd MV, nhấn nút tab trên bàn phím nó cũng ra lệnh như sau:

cd MVVMSample

Tiếp theo gõ để show toàn bộ branch:

git branch -a

Bạn sẽ thấy branch master, bai1… Mỗi branch sẽ chứa code của 1 bài. Ở đây ta chỉ quan tâm code bài 1 nên bạn cần chuyển qua code của bài 1 bằng cách gõ như sau:

git checkout bai1

Như vậy là bạn đã có code của bài hôm nay rồi đấy. Đơn giản đúng không?

Để triển khai mô hình MVVM, chúng ta cần nắm qua mô hình này xem nó hoạt động như thế nào.

Nếu tiếp cận cách đơn thuần như thế, thì cái table của bạn muốn có dữ liệu thì bạn phải tạo được data cho nó ở trong ViewController luôn. Rồi ngày qua ngày, cái table view này nó thêm nhiều tính năng mới. Ví dụ như khi chạm vào từng cell trên table thì phải kiểm tra xem cell đó là con gì, mở màn hình detail tương ứng. Hoặc trong cái view controller của bạn sếp yêu cầu phải lấy data từ trên server về thay vì tạo data set cứng trong nó. Bạn phải xử lý khi có data thì hiển thị như nào, khi không có data thì hiển thị như nào. Rồi một ngày đẹp trời nữa sếp bạn yêu cầu tao muốn hiển thị nhiều loại cell khác nhau, mỗi loại cell 1 loại data khác nhau… Code cứ dài càng dài, và càng khó chỉnh sửa sau này.

Vấn đề của MVC ở đây là xử lý hết logic trong View, thì code nhiều. Mà nhiều thì khó đọc, cáu, chửi thề, và cuối cùng là đấm sếp rồi nghỉ việc 

Thành phần mới này họ đặt tên là View Model – nghĩa là nó giao thông qua lại giữa view và model. Cái tên rất sát nghĩa đúng không? 

Thôi nói lan man. Ở đây tôi sẽ trình bày cách triển khai từng phần 1 cho dễ hiểu nha.

Đầu tiên bạn tạo mới 1 project bằng Xcode, đặt tên nó là gì cũng được. Vào file Main.storyboard và kéo thả 1 cái tableview, 1 cái title như hình:

Kéo thả title và tableview

Bước tiếp theo bạn tạo mới 1 file AnimalViewController kế thừa từ UIViewController, chọn nó quản lý cái file giao diện bạn vừa làm. Tiến hành kéo thả các liên kết từ view vào UIViewController này.

Giả sử tôi kéo thả tableview và được code như sau:

 @IBOutlet weak var tableView: UITableView!
 

Chưa xong, đã có tableview rồi thì phải tạo cell cho nó. Vậy bạn hãy tạo 1 cái lớp MyTableViewCell kế thừa từ UITableViewCell. Và lại kéo thả! Việc nhẹ lương cao nhỉ 

Hình nó như này:

Bên trái là label, bên phải là image.

Code như sau:

@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var animalImageView: UIImageView!

Rồi, và bây giờ để code clean thì bạn tạo cho tôi 1 cái hàm ở AnimalViewController có nội dung như sau:

/// Init table view
    private func initTableView() {
        tableView.register(UINib(nibName: "MyTableViewCell", bundle: nil), forCellReuseIdentifier: "MyTableViewCell")
        tableView.delegate = self
        tableView.dataSource = self
    }

Đầu tiên là bạn phải tạo cái model để chứa dữ liệu animal của tôi, bao gồm 1 cái tên và 1 cái hình. Tôi tạo code bằng 1 cái struct như sau:


struct Animal {
    let name: String
    let image: String
}

Struct vì nó là giá trị, không có dùng class à nha. Class để giành cho View model.

Rồi, bây giờ ta sẽ nghĩ ngay đến việc là cần 1 cái view model để xử lý các công việc sau:

  1. Tạo datasource cho tableview, chắc chắn phải ném nó vào hàm init của view model
  2. Tạo các hàm trả về cho delegate và datasource của tableview.

Cụ thể bạn tạo file AnimalViewModel và code như sau:

class AnimalViewModel {
    private var animals: [Animal]

    init() {
        animals = []

        // Create data source
        animals.append(Animal(name: "Alligator", image: "Alligator"))
        animals.append(Animal(name: "Anteater", image: "Anteater"))
        animals.append(Animal(name: "Armadillo", image: "Armadillo"))
    }

        return animals.count
    }

        return animals[indexPath.row]
    }
}

Code thì dài dòng, nhưng để đơn giản thì tôi tạo data ở local, bài sau tôi sẽ hướng dẫn lấy data từ đâu đó trên mạng nha. Các công việc như sau:

  1. Hàm init sẽ tạo datasource cho tableview
  2. Hàm numberOfRowsInSection tôi đặt giống hàm của table datasource, mục đích trả số row trên 1 section
  3. Hàm cellForRowAt cũng vậy, mục đích là để bind data cho cell nhá

Và quay lại file AnimalViewController, bạn viết thêm cho tôi dòng này:

var animalViewModel: AnimalViewModel!

Khi tạo xong view model rồi, thì bạn phải ném nó vào view controller. Sau đó, lại để cho code clean, bạn tạo 1 hàm xử lý view model cho nó. Code như sau:

private func bindViewModel() {
        animalViewModel = AnimalViewModel()
    }

Và bây giờ là cần xử lý cho table view, bạn viết đoạn code sau:

extension AnimalViewController: UITableViewDataSource, UITableViewDelegate {
        return animalViewModel.numberOfRowsInSection(section: section)
    }

        let cell = tableView.dequeueReusableCell(withIdentifier: "MyTableViewCell") as! MyTableViewCell
        cell.bindData(animal: animalViewModel.cellForRowAt(indexPath: indexPath))
        return cell
    }

        return 80
    }
}

Đoạn code trên khá sạch sẽ, view controller sẽ lấy data thông qua view model, và bạn méo cần quan tâm trong đó có gì, chỉ cần view model mày trả data cho tao là được. Do vậy, view controller đã không phải xử lý các logic xử lý model nữa. Khỏe hẳn ra, cứ như là lấy vợ về có người nấu cơm cho ăn ấy.

Tuy nhiên, ở ví dụ này tôi có 1 loạt bức ảnh màu trắng, bạn có thể xem trong mục Assets như hình:

Ảnh tôi chôm trên mạng

Do vậy để cho ứng dụng màu mè thì tôi viết thêm các extension tạo màu từ ảnh trắng này. Các bạn vào file Extensions để xem.

Trong UIImageView+Extensions tôi có code:

extension UIImageView {
  func setImageColor(color: UIColor) {
    let templateImage = self.image?.withRenderingMode(.alwaysTemplate)
    self.image = templateImage
    self.tintColor = color
  }
}

Đoạn này là tạo màu cho ảnh.

Trong UIColor+Extensions, là tạo màu random cho ảnh nó khác màu nhau nhá:

extension UIColor {
        return UIColor(
           red:   .random(),
           green: .random(),
           blue:  .random(),
           alpha: 1.0
        )
    }
}

                                                 Tuyển dụng swift lương cao cho bạn