MVVM 패턴의 큰 장점은
Controller의 역할 비중을 줄일 수 있다는 것이다.
가령, 기존의 MVC 패턴에서는
UI나 비지니스 로직 관련 모든 메서드를
Controller에서 구현했다.
따라서, 수많은 메서드를 구분하는 작업이
번잡하고 비효율적이었는데
MVVM 패턴을 통해 해결해보자.
간단하게, 숫자를 입력하면 세자리 수 단위를 나누는 콤마가 자동으로 찍히는 기능을 MVVM으로 구현해보자.
Observable, ViewModel, ViewController 클래스 코드로 이루어진다.
기존의 MVC 패턴을 상상하고 비교하면서 살펴보자.
Observable 클래스
"너희 둘, Controller랑 ViewModel끼리 직접 소통도 가능하겠지만, 내가 더 매끄럽게 도와줄께!"
"처음 Binding할 때랑 변수 값이 바뀔 때, Controller가 알려준 매서드를 실행할께!"
- MVVM 패턴은 Model, View, ViewModel로 이루어져있다.
- 기존 Controller는 오로지 View의 UI에 데이터를 연결해주는 최소한의 역할을 한다.
- 그리고 각종 비지니스 로직은 ViewModel이 관리한다.
- 이 때, Controller와 ViewModel 간 소통 창구가 필요한데, 이를 Observable이 해줄 수 있다.
(지금은 아래 코드를 훑어만 보고, 게시글을 읽어가며 필요할 때만 참고해보자)
class CObservable<T> {
private var listener: ((T) -> Void)?
var value: T {
didSet {
listener?(value)
}
}
init(_ value: T) {
self.value = value
}
func bind(_ closure: @escaping (T) -> Void) {
closure(value)
listener = closure
}
}
ViewModel 클래스
"Controller야, 이제 복잡한 로직들은 나에게 맡겨!"
"Observer야, 우선 변수 좀 저장해주고, 내가 변수 값을 바꾸면 자동으로 didSet 함수 좀 실행해주라!"
- 실 데이터로 쓰일 변수와 비지니스 로직을 ViewModel에서 구현한다.
- 각 변수들은 변형될 때마다 반응할 수 있도록 Observable 타입에 제너럴 매개변수 값을 넣어 초기화한다.
- ViewModel에서는 UIKit을 다루지 않기 때문에 import 하지 않는다.
class NumberViewModel {
var pageNumber: CObservable<String> = CObservable("3000")
func changePageNumberFormat(text: String) {
// 숫자 세자리마다 쉼표 표시
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
// 쉼표가 찍힌 숫자 스트링 값을 인트 값으로 바꾸는 과정
let text = text.replacingOccurrences(of: ",", with: "")
guard let number = Int(text) else { return }
let result = numberFormatter.string(for: number)!
pageNumber.value = result
}
}
ViewController 클래스
"ViewModel아, 값이 바뀔 때마다 실행될 클로져를 Observer한테 전달해줘!"
"Observer야, 우선 변수 좀 저장해주고, 내가 변수 값을 바꾸면 자동으로 didSet 함수 좀 실행해주라!"
- Data Binding 과정을 통해 viewDidLoad 시점에서 변수를 초기화하는 클로져를 작성한다.
- 그리고 이 클로져는 Observer 클래스를 통해 변수값이 바뀔 때마다 didSet을 통해 실행된다.
- 필요할 때마다 ViewModel 클래스로부터 비지니스 로직과 관련된 매서드들을 호출한다.
class NumberViewController: UIViewController {
@IBOutlet weak var numberTextField: UITextField!
var viewModel = NumberViewModel()
override func viewDidLoad() {
super.viewDidLoad()
bindData()
}
func bindData() {
// numberTextField.text = "3,000"
viewModel.pageNumber.bind { value in
self.numberTextField.text = value
}
}
func configureViews() {
numberTextField.addTarget(self, action: #selector(numberTextFieldChange), for: .editingChanged)
}
@objc
func numberTextFieldChange() {
guard let text = numberTextField.text else { return }
viewModel.changePageNumberFormat(text: text)
}
}