[SwiftUI] Design Pattern : MVVM
SwiftUI에서 MVVM 패턴은 불필요한 것이란 얘기가 많다. MVVM 패턴은 View와 Model 사이에서 데이터를 주고 받을 때 ViewModel을 사용하는 패턴인데, SwiftUI에서는 SwiftUI.View에 ViewModel의 data binding이 포함되어 있기 때문이다. 그렇기 때문에 SwiftUI에서 ViewModel을 쓰는 것은 불필요한 레이어를 하나 더 끼워 넣는 꼴이기 때문에 불필요하다는 얘기가 많다.
하지만, Swift를 공부하는 입장이라면 적어도 MVVM 패턴이 어떤 것인지 알아야 할 필요가 있기 때문에 MVVM 패턴에 대해 알아보도록 하겠다.
Model + View + ViewModel
MVVM은 Model, View, ViewModel로 구성된 design pattern이다.
Model : 흔히 알고 있는 데이터에 관한 것. 데이터를 어떻게 가지고 있을지만 생각하면 된다
View : UI에 관한 것. 코드 재사용성이 중요하다. 컴포넌트를 적당히 잘 나누어 중복되는 코드를 줄이는 것이 중요하다.
struct ButtonView: View {
var body: some View {
Button(action: {
//버튼 클릭시 실행되는 코드
print("버튼 클릭")
}) {
//버튼의 보여지는 UI코드
HStack{
Text("Log in")
Image(systemName: "arrow.right.circle")
}
}
.buttonStyle(MyButtonStyle())
}
}
//재사용이 가능한 Button Style
//Button의 경우 통일되는 style이 자주 사용되는 경우가 많다
//각각의 코드에 계속 같은 코드를 붙이는 것은 코드가 깔끔하지 않고, 유지보수성이 떨어진다
//그런 경우 ButtonStyle을 따로 만들어서 적용한다
struct MyButtonStyle: ButtonStyle{
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.font(.system(size: 30, weight: .bold, design: .rounded))
.foregroundColor(.white)
.padding()
.background(Color.pink)
.clipShape(Capsule())
.overlay(Capsule().stroke(Color.pink, lineWidth: 5)) //테두리 만드는 거 : overlay
}
}
struct ContentView: View {
let personList: [Person] = [Person(name: "", age: 23), Person(name: "Jeon", age: 28)]
var body: some View {
ScrollView {
ForEach(personList, id: ./self) { person in
PersonListItemView(person: person)
}
}
}
}
struct PersonListItemView: View {
let person: Person
var body: some View {
VStack(alignment: .leading) {
Text("Name: \(person.name)")
Text("Age: \(person.age)")
}
.padding(16)
}
}
ViewModel : 앱의 핵심적인 Business logic을 담고 있다. UI 코드로부터 완전한 분리
View는 ViewModel을 바라보고 있고, ViewModel은 Model을 바라보고 있다.
Why MVVM ?
기존 UIKit에서는 MVC pattern이 지배적이었으나, SwiftUI가 나온 이후로 MVVM이 더욱 fit한(?) pattern이다.
MVC pattern의 View Controller는 View에 대한 코드, Controller에 대한 코드를 포함하고 있었다. Business logic 말고도 View에 대한 수정, 설정 등에 대한 코드를 가지고 있었다는 뜻이다. 즉, View와 Controller 계층이 분리되지 않았다. 그리고 이는 코드가 길어진다는 단점이 야기한다.
하지만, SwiftUI와 MVVM pattern에서 View와 ViewModel의 완전한 분리가 이루어졌다. ViewModel에서는 Business logic만을, View에서는 UI에 대한 내용만을 담고 있기 때문에 각 계층의 모듈화가 이루어졌고 테스트가 용이하다는 장점을 야기한다.
SwiftUI와 MVVM의 특징
SwiftUI의 특징 중 하나는 Reactive Programming, 반응형 프로그래밍이라는 것이다. 변화에 따른 데이터 전달 및 이벤트 실행이 반응적으로 이루어진다. 그리고 이를 가능하게 하는 것이 MVVM pattern의 data binding이다. data binding이란 data 제공하는 자와 사용하는 자 사이의 연결 동기화를 말한다.
View는 ViewModel을 소유하고 ViewModel은 Model을 소유한다.
다른 말로, View는 VIewModel을 바라보고 있고, ViewModel은 View에 의해 Observed되고 있다. 바라보고 있기 때문에 특정 프로퍼티의 변화를 감지하여 Reactive Programming을 가능하게 한다.
ViewModel의 특정 프로퍼티 (Published 되고 있는 프로퍼티)에 변화가 생기면, data 변화를 유저에게 보여주기 위해 View를 다시 그린다.
View | ViewModel |
@ObservedObject | @Published |
예시 코드
View
import SwiftUI
struct ContentView: View {
@ObservedObject var viewmodel = ViewModel()
var body: some View {
VStack{
Text("Hi! \(viewmodel.person.name)'s age is \(viewmodel.person.age)")
.padding()
Button(action: {
viewmodel.addAge()
}) {
Text("Add an Age")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
View에서는 완전히 UI에 관한 코드만 담겨있다는 것을 알 수 있다.
또한, viewmodel을 가져올 때 @ObservedObject로 가져온다.
Model
struct Person {
let name: String
var age: Int
}
ViewModel
import SwiftUI
class ViewModel: ObservableObject {
@Published var person: Person
init() {
self.person = Person(name:"park", age:50)
}
func addAge() {
self.person.age += 1
}
}
person을 Published한다고 설정해준다.
또한, ViewModel에서 class를 지정할 때 init(), 초기값이 필요하다.
ViewModel에 여러가지 로직이 담긴다.
코드 흐름
- View에서 button을 클릭한다.
- button의 action 코드가 발동한다. 이는 적힌대로, viewmodel (ViewModel을 받는다) 의 addAge() 함수를 호출한다.
- addAge() 함수 로직대로 실행된다. 이는 ViewModel에 있는 addAge함수로, person의 age 프로퍼티에 1을 더한다.
- Published의 프로퍼티가 변경되었다. 사용자에게 알려주기 위해 View를 다시 그린다.
- 변경된 값으로 View를 다시 그렸고, 우리는 변경된 값을 본다.
MC1 밥몽어스 때도 분명 MVVM pattern을 썼겠지만, 제대로 된 이해없이 진행해서인지 똥인지 된장인지도 모르고 코드를 쳤다. 좋은 정리글을 보고 손으로 직접 써가면서 공부하니 이제서야 이해가 되는 느낌이다. 역시 나는 손으로 써가면서 공부해야 하는 타입인가 ..
참조: https://www.vadimbulavin.com/modern-mvvm-ios-app-architecture-with-combine-and-swiftui/