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))
}
}
- Thuộc tính displayName có giá trị là tên của category sau khi đã được transform.
- Tạo 1 closure để phát event.
- func thực hiện logic transform name trước khi hiển thị cho người dùng.
- 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")
}
}
- Khai báo 1 viewModel cho View.
- 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.
- 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()
}
}
- displayName được sửa lại thành kiểu Binding<String>
- 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")
}
}
- 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.