Model-View-ViewModel

역할
- Model : MVC 패턴과 마찬가지로 앱의 데이터를 담는다.(주로 구조체)
- View : UI를 화면에 표시한다.
- View Model : Model이 가지고 있는 데이터를 View에 표시할 수 있는 데이터로 변환한다.(주로 클래스)
특징
- 뷰 컨트롤러의 역할이 너무 많은 MVC 패턴의 단점을 보완한다.
- 유닛 테스트 코드를 작성하기에 좋다.
- 뷰에 들어가는 데이터를 추가하거나 변경하기 수월하다.
- 뷰 모델과 뷰는 1:n의 관계를 갖는다.
- 뷰는 뷰 모델을 바라만 보고 뷰 모델은 뷰를 변경하는 것을 지시하지 않는다. 뷰 모델의 값이 업데이트되면 자동으로 뷰에 적용된다.(Binding)
Binding
View To View Model
struct LoginViewModel {
var username: String = ""
var password: String = ""
}
class BindingTextField: UITextField {
var textChanged: (String) -> Void = { _ in }
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
addTarget(self, action: #selector(textFieldDidChanged), for: .editingChanged)
}
func bind(callback: @escaping (String) -> Void) {
textChanged = callback
}
@objc func textFieldDidChanged(_ textField: UITextField) {
if let text = textField.text {
textChanged(text)
}
}
}
let usernameTextField = BindingTextField()
usernameTextField.placeholder = "Enter username"
usernameTextField.backgroundColor = UIColor.lightGray
usernameTextField.borderStyle = .roundedRect
usernameTextField.bind { [weak self] text in
self?.loginVM.username = text
}
View Model To View
class Dynamic<T> {
typealias Listener = (T) -> Void
var listener: Listener?
var value: T {
didSet {
listener?(value)
}
}
func bind(callback: @escaping (T) -> Void) {
self.listener = callback
}
init(_ value: T) {
self.value = value
}
}
struct LoginViewModel {
var username = Dynamic("")
var password = Dynamic("")
}
override func viewDidLoad() {
super.viewDidLoad()
loginVM.username.bind { [weak self] text in
self?.usernameTextField.text = text
}
loginVM.password.bind { [weak self] text in
self?.passwordTextField.text = text
}
}
Tip
- 뷰 모델을 하나의 뷰에만 사용한다면 configure 메소드를 뷰 모델 안에 구현하고 여러 뷰에 사용한다면 각 뷰에 구현하는 것이 좋을 수도 있다.
- 무조건 MVVM이 좋은 것은 아니다. 경우에 따라 MVC 패턴을 사용하는 것이 더 나은 방법일 수도 있고 MVVM과 다른 디자인 패턴을 섞어서 사용하는 방법도 있다.
참고
Design Patterns by Tutorials: MVVM