MVVM + RxSwift 이해하기 쉬운 예제

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) 뷰를 갱신하게 한다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다