M : Model, 데이터 소스
V : View, 보이는것만 처리, ViewModel을 포함
VM : View Model, 보이는 값을 처리하기 위한 비지니스 로직, Model을 포함
Rx 거부감이 있으면 KVO(Key-Value Observing) 방식 또는 클로저, Notification 방식도 가능함
MVVM 패턴을 사용하는 이유는 코드 분리, 재사용하고 유지보수를 쉽게 하기 위함이다.
여러개의 뷰에서 뷰모델을 가져다 쓸 수 있고 어떤 뷰모델에서도 모델을 가져다 사용할 수 있다.
모델에서는 뷰모델의 존재를 모르고 뷰모델에서는 뷰의 존재를 모른다.
// ViewController class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. if let v: View = UIView.loadFromNib() { v.add(self.view) } } }
// View class View: UIView { @IBOutlet var label: UILabel! private let viewModel = ViewModel() private let disposeBag = DisposeBag() override init(frame: CGRect) { super.init(frame: frame) } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } override func awakeFromNib() { super.awakeFromNib() // ViewModel의 result 값이 변경되는 것을 구독하여 변경되면 View의 label을 업데이트 // 스케쥴러는 메인 스레드 큐 (drive) viewModel.result.drive { val in self.label.text = val print(self.viewModel.value()) }.disposed(by: disposeBag) viewModel.loadLabel() } func add(_ superView: UIView) { superView.addSubview(self) self.translatesAutoresizingMaskIntoConstraints = false var constraints = [NSLayoutConstraint]() constraints.append(superView.topAnchor.constraint(equalTo: self.topAnchor)) constraints.append(superView.leadingAnchor.constraint(equalTo: self.leadingAnchor)) constraints.append(superView.trailingAnchor.constraint(equalTo: self.trailingAnchor)) constraints.append(superView.bottomAnchor.constraint(equalTo: self.bottomAnchor)) NSLayoutConstraint.activate(constraints) } } extension UIView { class func loadFromNib<T>() -> T? { let identifier = String(describing: T.self) let view = Bundle.main.loadNibNamed(identifier, owner: self, options: nil)?.first return view as? T } }
// Model struct Model { var label: String = "model" }
// ViewModel class ViewModel { private var m = Model() private var _result: BehaviorRelay<String> var result: Driver<String> { return _result.asDriver() } init() { _result = BehaviorRelay<String>(value: "init") } func value() -> String { return _result.value } func changed() { _result.accept(m.label) } func loadLabel() { DispatchQueue.global().asyncAfter(deadline: .now()+2) { self.m.label = "value from server or app db" self.changed() } } }
대략적인 설명은 View의 label 값을 갱신하기 위해 MVVM 패턴과 Rx를 사용했다.
ViewModel에서 Driver 사용은 메인스레드에서 View의 label을 갱신하기 위함이다.
View의 label을 갱신하기 위해 ViewModel의 변경사항을 구독하고
ViewModel에서 비지니스 로직을 계산하고 변경사항이 생기면 알려줘서(accept) 뷰를 갱신하게 한다.