MVVM 패턴
Model - View - ViewModel로 이루어진 디자인 패턴이다.
Model | 데이터를 처리 |
View | 사용자에게 보여지는 UI |
ViewModel | View를 표현하기 위해 만들어진 View를 위한 모델이다. 비즈니스 로직 등을 포함하고 있다. |
✅ MVVM 패턴에 대해서는 나중에 자세하게 다른 글에서 다루겠다.
예제 1 - MVVM 패턴을 이용한 시계
// Model
class Clock {
static var currentTime: (() -> String) = {
let today = Date()
let hours = Calendar.current.component(.hour, from: today)
let minutes = Calendar.current.component(.minute, from: today)
let minStr = String(format: "%02d", minutes)
let seconds = Calendar.current.component(.second, from: today)
let secStr = String(format: "%02d", seconds)
return "\(hours):\(minStr):\(secStr)"
}
}
단순히 시간 표현을 위한 모델이다.
// View
class ViewController: UIViewController {
let closureLabel: UILabel = {
let label = UILabel()
label.text = "Closure Time"
label.textColor = .black
label.backgroundColor = .clear
label.font = .boldSystemFont(ofSize: 30)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let closureTimeLabel: UILabel = {
let label = UILabel()
label.text = "Unknown"
label.textColor = .black
label.backgroundColor = .clear
label.font = .systemFont(ofSize: 15)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let observerLabel: UILabel = {
let label = UILabel()
label.text = "Observer Time"
label.textColor = .black
label.backgroundColor = .clear
label.font = .boldSystemFont(ofSize: 30)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let observerTimeLabel: UILabel = {
let label = UILabel()
label.text = "Unknown"
label.textColor = .black
label.backgroundColor = .clear
label.font = .systemFont(ofSize: 15)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
self.setView()
self.setAutoLayout()
}
private func setView() {
self.view.backgroundColor = .white
self.view.addSubview(self.closureLabel)
self.view.addSubview(self.closureTimeLabel)
self.view.addSubview(self.observerLabel)
self.view.addSubview(self.observerTimeLabel)
}
private func setAutoLayout() {
self.closureLabel.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor).isActive = true
self.closureLabel.centerYAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerYAnchor, constant: -150).isActive = true
self.closureTimeLabel.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor).isActive = true
self.closureTimeLabel.topAnchor.constraint(equalTo: self.closureLabel.bottomAnchor, constant: 15).isActive = true
self.observerLabel.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor).isActive = true
self.observerLabel.centerYAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerYAnchor, constant: 150).isActive = true
self.observerTimeLabel.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor).isActive = true
self.observerTimeLabel.topAnchor.constraint(equalTo: self.observerLabel.bottomAnchor, constant: 15).isActive = true
}
}
사용자에게 보여질 뷰이다.
1️⃣ 클로저를 이용한 MVVM
// ViewModel
class ClockViewModel {
var didChangeTime: ((ClockViewModel) -> Void)?
var closureTime: String {
didSet {
didChangeTime?(self)
}
}
init() {
closureTime = Clock.currentTime()
}
func checkTime() {
closureTime = Clock.currentTime()
}
}
var didChangeTime: ((ClockViewModel) -> Void)?
➡️ 수행될 어떠한 동작을 담을 didChangeTime이라는 클로저 변수를 생성한다.
var closureTime: String {
didSet {
didChangeTime?(self)
}
}
➡️ closureTime 프로퍼티의 프로퍼티 옵저버 didSet을 통해 이벤트를 감지하여 어떠한 동작을 실행한다.
init() {
closureTime = Clock.currentTime()
}
➡️ 생성자를 통해 뷰모델 생성시 closureTime 프로퍼티에 현재 시간을 저장한다.
func checkTime() {
closureTime = Clock.currentTime()
}
➡️ 호출 시 closureTime 프로퍼티에 현재 시간을 저장하는 checkTime() 함수이다.
ViewModel 구현을 마치면 View에서 동작을 구현하면 된다.
private var viewModel = ClockViewModel()
override func viewDidLoad() {
super.viewDidLoad()
self.setView()
self.setAutoLayout()
self.startTimer()
self.setBindings()
}
private func startTimer() {
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
self?.viewModel.checkTime()
}
}
private func setBindings() {
viewModel.didChangeTime = { [weak self] viewModel in
self?.closureTimeLabel.text = viewModel.closureTime
}
}
private func startTimer() {
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
self?.viewModel.checkTime()
}
}
➡️ 1초마다 viewModel의 checkTime()을 호출한다.
checkTime 함수는 현재 시간을 담아두는 행위(closureTime = Clock.currentTime())를 하므로, 1초마다 closureTime에 현재 시간을 저장한다. viewModel에서 정의한 closureTime 프로퍼티는 이벤트(여기서는 현재 시간을 담아두는 행위/이벤트)를 감지하면 didSet을 호출하므로 didChangeTime이라는 동작이 실행된다.
private func setBindings() {
viewModel.didChangeTime = { [weak self] viewModel in
self?.closureTimeLabel.text = viewModel.closureTime
}
}
➡️ setBindings 함수는 didChangeTime가 어떤 행위를 하는지 정의한 함수이다.
startTimer 함수가 실행 되면서 1초마다 viewModel에서 정의한 checkTime 함수가 호출되고,
checkTime은 closureTime 프로퍼티에 현재 시간을 저장하는 행위를 하고,
closureTime의 didSet이 이러한 행위를 감지하여
setBindings()에서 정의한 didChangeTime이라는 동작이 실행된다.
여기서 didChangeTime(동작)은 closureTimeLabel의 텍스트를 현재 시간으로 출력해주는 동작이다.
2️⃣ Observable(관찰자)를 이용한 MVVM
상태의 변화를 감지할 관찰자 클래스이다. 모든 타입을 사용할 수 있도록 제네릭으로 생성한다.
class Observable<T> {
typealias Listner = (T) -> Void
var listner: Listner?
var value: T {
didSet {
self.listner?(value)
}
}
init(_ value: T) {
self.value = value
}
func bind(listener: Listner?) {
listener?(value)
self.listner = listener
}
}
typealias Listner = (T) -> Void
➡️ typealias를 이용하여 (T) -> Void 클로저를 갖는 Listner라는 타입을 지정한다.
var listner: Listner?
➡️ typealias를 통해 지정한 Listner 타입을 갖는 즉, 동작을 담아 놓는 listner를 생성한다.
var value: T {
didSet {
self.listner?(value)
}
}
➡️ value 프로퍼티에 프로퍼티 옵저버 didSet을 통해 이벤트를 감지하여 어떠한 동작을 실행한다.
init(_ value: T) {
self.value = value
}
➡️ 생성자를 통해 뷰모델 생성시 value 프로퍼티에 인자로 받아온 value를 저장한다.
func bind(listener: Listner?) {
self.listner = listener
listener?(value)
}
➡️ 호출 시 동작을 listener에 저장하고 동작을 실행하는 bind 함수이다.
// ViewModel
var observableTime: Observable<String> = Observable("")
init() {
closureTime = Clock.currentTime()
}
func checkTime() {
observableTime.value = Clock.currentTime()
}
var observableTime: Observable<String> = Observable("")
➡️ Observable 객체를 생성한다. 제네릭 타입으로 선언했기 때문에 타입을 명시하고 초기값을 포함하여 생성한다.
func checkTime() {
observableTime.value = Clock.currentTime()
}
➡️ Observable에서 정의한 value를 호출하여 현재 시간을 저장한다. value 프로퍼티의 프로퍼티 옵저버 didSet을 통해 이벤트를 감지하여 어떠한 동작을 실행한다. 여기서 이벤트는 현재 시간을 저장하는 이벤트이다.
private var viewModel = ClockViewModel()
override func viewDidLoad() {
super.viewDidLoad()
self.setView()
self.setAutoLayout()
self.startTimer()
self.setBindings()
}
private func startTimer() {
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
self?.viewModel.checkTime()
}
}
private func setBindings() {
viewModel.observableTime.bind { [weak self] text in
self?.observerTimeLabel.text = text
}
}
private func setBindings() {
viewModel.observableTime.bind { [weak self] text in
self?.observerTimeLabel.text = text
}
}
➡️ setBindings 함수는 value가 어떤 행위를 하는지 정의한 함수이다.
startTimer 함수가 실행 되면서 1초마다 viewModel에서 정의한 checkTime 함수가 호출되고,
checkTime은 value 프로퍼티에 현재 시간을 저장하는 행위를 하고,
value의 didSet이 이러한 행위를 감지하여
setBindings()에서 정의한 bind가 실행된다.
bind가 실행되면 동작을 listner에 저장하고 동작을 실행한다.
여기서 동작은 observerTimeLabel의 텍스트를 현재 시간으로 출력해주는 동작이다.
Reference
'iOS > Design Pattern' 카테고리의 다른 글
[iOS/Design Pattern] Clean Architecture를 알아보자. (0) | 2024.08.26 |
---|---|
[iOS/Design Pattern] 코디네이터 패턴 (0) | 2024.08.02 |
[iOS/Design Pattern] MVVM에 대한 나의 고찰 (0) | 2024.05.01 |
[iOS/Design Pattern] MVVM 패턴 이해해보기 (0) | 2024.01.05 |
[iOS/Design Pattern] 예제로 알아보는 MVVM패턴 - 2 (0) | 2023.08.08 |