MVVM with swift P1

by Hoang Anh Tuan
512 views

Bài viết này, mình sẽ nói về MVVM.
Bài viết khá dài, nên sẽ được chia thành 2 phần.

Content:

Phần 1:

  • Mô hình MVVM?
  • Demo:
    • MVVM with Closure/Callback
    • Optimize MVVM with Closure/Callback

Phần 2:

  • Demo:
    • MVVM with RxSwift
  • Tác dụng của MVVM
  • MVVM vs MVP?
  • Kết luận

MVVM là gì?

MVVM gồm 3 phần:

Model:

Tương tự Model Layer của MVC, MVP.

View:

Tương tự so với View Layer của MVP.

ViewModel:

  • Đây là phần khác biệt của MVVM so với MVP.
  • ViewModel nằm giữa Model và View, có tác dụng là cầu nối để giao tiếp giữa Model và View.
  • ViewModel còn có tác dụng xử lí các logic convert, format data trước khi View hiển thị data đó cho người dùng.
  • Ngoài ra, ViewModel cũng có thể chứa các logic như update database, xử lí networking, … .Tuy nhiên, nên tách các logic này ra thành các class khác để đảm bảo nguyên tắc Single Responsibility Principle.

Note:

  • View không tương tác trực tiếp với Model. View chỉ có thể tương tác với Model thông qua ViewModel.
  • ViewModel không biết gì về View.
  • MVVM giao tiếp giữa ViewModel và View bằng cách Observer (thường được gọi là binding data).
  • ViewModel cũng không được import UIKit để tách biệt phần logic ra khỏi phần UI.

Demo MVVM with Closure/Callback:

Model Layer

import Foundation

struct Category {
    let id: Int
    var name: String
}

ViewModel Layer:

Giờ thì hãy tạo 1 ViewModel để thực hiện các logic xử lí data trước khi View hiển thị cho người dùng:

import Foundation

class HomeViewModel {
    private var category: Category
    
    // 1
    var displayName: String {
        return transformName(category.name)
    }
    
    // 2
    var onSuccess: ((String) -> Void)?
    
    init(category: Category) {
        self.category = category
    }
    
    // 3
    func transformName(_ name: String) -> String {
        let newName = name.enumerated().map { (index, character) -> String in
            if index % 2 == 0 {
                return character.uppercased()
            } else {
                return character.lowercased()
            }
        }
        
        return newName.joined()
    }
    
    // 4
    func changeCategoryName(_ newName: String) {
        category.name = newName
        onSuccess?(transformName(newName))
    }
}
  1. Thuộc tính displayName có giá trị là tên của category sau khi đã được transform.
  2. Tạo 1 closure để phát event.
  3. func thực hiện logic transform name trước khi hiển thị cho người dùng.
  4. func thực hiện update name cho category, sau khi update thành công thì phát ra event chứa giá trị mới.

View Layer

import UIKit

class HomeView: UIViewController {
    @IBOutlet private weak var categoryNameLabel: UILabel!
    
    // 1
    private var viewModel: HomeViewModel?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 2
        let category = Category(id: 1, name: "Hollywood")
        viewModel = HomeViewModel(category: category)
        categoryNameLabel.text = viewModel?.displayName
        
        // 3
        viewModel?.onSuccess = {
            self.categoryNameLabel.text = $0
        }
        
    }

    @IBAction func didTapButtonChangeCategory(_ sender: Any) {
        viewModel?.changeCategoryName("vietnam")
    }
}
  1. Khai báo 1 viewModel cho View.
  2. Khởi tạo viewModel và hiển thị name sau khi transform lên giao diện để hiển thị cho người dùng.
  3. Thực hiện observer event được phát ra từ viewModel và update UI.

Optimize Closure?

Với cách sử dụng closure ở trên thì bạn đã tạo ra 1 binding giữa View và Model. Nhưng nếu có nhiều thuộc tính phải transform, thì chúng ta phải tạo ra nhiều closure ở ViewModel.
Liệu có cách nào có thể optimize chúng không?

Câu trả lời là sử dụng Generic. Tạo ra 1 class generic để tự động phát ra 1 event khi được set 1 value mới:

class Binding<T> {
    typealias Listener = (T) -> Void
    var listener: Listener?
    
    var value: T {
        didSet {
            listener?(value)
        }
    }
    
    init(value: T) {
        self.value = value
    }
    
    func bind(listener: Listener?) {
        self.listener = listener
        self.listener?(value)
    }
}

ViewModel Layer khi đó sẽ được update lại như sau:

class HomeViewModel {
    var category: Category
    // 1
    public var displayName: Binding<String>
    
    init(category: Category) {
        self.category = category
        
        let newName = category.name.enumerated().map { (index, character) -> String in
            if index % 2 == 0 {
                return character.uppercased()
            } else {
                return character.lowercased()
            }
        }
        self.displayName = Binding<String>(value: newName.joined())
    }
    
    func changeCategoryName(_ newName: String) {
        category.name = newName
        // 1
        displayName.value = transformName(newName)
    }
    
    func transformName(_ name: String) -> String {
        let newName = name.enumerated().map { (index, character) -> String in
            if index % 2 == 0 {
                return character.uppercased()
            } else {
                return character.lowercased()
            }
        }
        
        return newName.joined()
    }
}
  1. displayName được sửa lại thành kiểu Binding<String>
  2. Mỗi khi thay đổi tên, chỉ cần set lại value cho displayName, thì displayName sẽ tự động phát ra 1 event chứa newName.

View Layer sau khi optimize sẽ trở thành:

class HomeView: UIViewController {
    @IBOutlet private weak var categoryNameLabel: UILabel!
    private var viewModel: HomeViewModel?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let category = Category(id: 1, name: "Hollywood")
        viewModel = HomeViewModel(category: category)
        
        // 1
        viewModel?.displayName.bind(listener: {
            self.categoryNameLabel.text = $0
        })
        
    }

    @IBAction func didTapButtonChangeCategory(_ sender: Any) {
        viewModel?.changeCategoryName("vietnam")
    }
}
  1. Thực hiện observer event được phát ra từ displayname của ViewModel và thực hiện update UI với giá trị mới nhận được.

Note: Để tránh bài viết quá dài, thì mình sẽ không add ảnh kết quả run demo vào đây.

Kết luận:

Qua phần 1, có thể thấy 1 lợi ích của MVVM so với MVP là không cần tạo delegate để giao tiếp.
Phần 1 sẽ chỉ dừng ở đây thôi, mình sẽ cùng tìm hiểu rõ hơn MVVM ở phần 2.

Leave a Comment

* By using this form you agree with the storage and handling of your data by this website.