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

2085

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:

Lập trình IOS: Triển khai MVVM cho prject swift(phần 1)
Danh sách các loại động vật

Đầu tiên là tải source code của tôi tại đây:

https://github.com/codetoanbug/MVVMSample

Tôi hướng dẫn các bạn tải code về bằng git 1 xíu nhé. Là git command line nha các bạn.

  Rx-MVVM(1): Cấu trúc project – lib manager
  Rx-MVVM(2): Cấu trúc project – quản lý thư viện sử dụng trong dự án

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

git clone https://github.com/codetoanbug/MVVMSample.git

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.

Hãy tiếp cận 1 cách đơn giản nhất nhé. Mặc định khi bạn tạo 1 project thì Xcode nó tự render các file theo tiêu chuẩn MVC của họ. M là Model, V là View, còn C là Controller. Tuy nhiên, ở đây chúng ta sẽ hay xử lý logic để lấy dữ liệu cho view và hiển thị view chung vào ViewController.

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 

Có rất nhiều developer đã căng thẳng như thế, tóc bạc đi nhiều sau nhiều năm code MVC thuần. Rồi họ bắt đầu bực bội và muốn thay đổi điều gì đó để cho công việc đơn giản hơn. Họ nghĩ ngay “tại sao không đưa các xử lý logic về data sang 1 file khác, còn view chỉ đơn thuần là find data vào thôi?” Và rồi họ quyết định làm điều đó. Ra ban công làm khói, ngắm hàng xóm và tiếp tục đọc nhé.

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:

Lập trình IOS: Triển khai MVVM cho prject swift(phần 1)
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:

Lập trình IOS: Triển khai MVVM cho prject swift(phần 1)
Bên trái là label, bên phải là image.

Nhớ auto layout cho nó nhé. Không biết auto layout thì tôi đấm luôn. Chú ý cái khoanh đỏ tôi lười nên hay lấy cái identifier này trùng với tên class cell. Để cho dễ nhớ và copy cho tiện. Mục đích của nó là sau tái sử dụng.

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
    }

Rất basic, ở đây nó có ý nghĩa là table đăng ký cell để tí dùng. Kế thừa các hàm delegate và datasource. Những công việc mà newbie nào cũng cần biết nhỉ. Sau đó ném cái hàm vào trong hàm viewDidLoad nhé. Vẫn chưa thấy view model đâu. Bây giờ là chuyên mục chính này.

Đầ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"))
    }

    func numberOfRowsInSection(section: Int) -> Int {
        return animals.count
    }

    func cellForRowAt(indexPath: IndexPath) -> Animal {
        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()
    }

Ở trên chỉ độc 1 dòng code là tạo view model. Tuy nhiên các dự án thực tế nó có nhiều setting khác, như các closure của view model. Nhưng khoan quan tâm, tôi sẽ trình bày ở các bài sau nhé.

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

extension AnimalViewController: UITableViewDataSource, UITableViewDelegate {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return animalViewModel.numberOfRowsInSection(section: section)
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "MyTableViewCell") as! MyTableViewCell
        cell.bindData(animal: animalViewModel.cellForRowAt(indexPath: indexPath))
        return cell
    }

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        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:

Lập trình IOS: Triển khai MVVM cho prject swift(phần 1)
Ả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 {
    static func random() -> UIColor {
        return UIColor(
           red:   .random(),
           green: .random(),
           blue:  .random(),
           alpha: 1.0
        )
    }
}

Ok, vậy là cơ bản đã xong rồi đó. Trong bài tiếp theo tôi sẽ làm thêm về phần request lên server nhé.

Bài viết gốc được đăng tải tại codetoanbug.com

Có thể bạn quan tâm:

Xem thêm Việc làm ios hấp dẫn trên TopDev

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