SwiftUI에서의 MVVM 패턴을 예제로 이해하기
SwiftUI
에서는 MVVM(Model-View-ViewModel)
패턴이 권장되는 디자인 패턴 중 하나입니다. 이 패턴은 SwiftUI
의 선언적 프로그래밍 스타일에 잘 맞으며, MVVM
을 통해 데이터 흐름과 UI 업데이트를 더 잘 관리하고, 코드의 모듈화와 유지보수성을 유지할 수 있습니다.
단계별 예제
먼저 데이터 구조를 나타내는 Model
을 정의합니다
// MARK: Modelstruct Person:Identifiable { var id = UUID() var name:String = "" var sex:String = ""}
그다음, 페이지 응답을 트리거할 수 있는 ViewModel
을 정의합니다. 이 예제에서는 데이터의 추가, 삭제, 수정, 조회를 시뮬레이션합니다.
// MARK: ViewModelclass PersonViewModel:ObservableObject {
// Person List @Published var persons:[Person] = []
@Published var selected:Person?
// Query Person List func fetchList(){ // Here it is assumed that you get the data from the server or a source such as CoreData and assign it to self.persons }
// Insert Person func insertPerson(){ // Simulation of additional operations if let person = selected { self.persons.append(person) }
}
// Update Person func updatePerson(){ // Simulation of modification operations if let person = selected, let index = persons.firstIndex(where: { $0.id == person.id }) { persons[index] = person } }
// Delete Person func deletePerson(offsets: IndexSet){ // Here it is only deleted from the page list, if the data is still stored elsewhere, then it needs to be deleted together with the new related logic offsets.forEach { persons.remove(at: $0) } }
}
마지막으로 ViewModel
을 통해 데이터를 뷰와 연결하여 뷰 계층을 구성합니다.
// List Viewstruct SwiftUIMVVM: View {
@StateObject var personVm:PersonViewModel = PersonViewModel()
@State var showEdit = false
var body: some View {
NavigationStack { List { ForEach(personVm.persons) { person in Button(action: { personVm.selected = person showEdit = true }, label: { personView(person: person) })
} .onDelete(perform: { indexSet in personVm.deletePerson(offsets: indexSet) }) } .listRowSpacing(10) .toolbar { ToolbarItem(placement: .topBarTrailing) { Button("", systemImage: "plus") { showEdit = true } } } .sheet(isPresented: $showEdit, onDismiss: { personVm.selected = nil }, content: { NavigationStack { PersonEditView() .environmentObject(personVm) } .presentationDetents([.medium])
}) }
}
// Display list information for each person // Here we have abstracted the code to increase its readability func personView(person:Person) -> some View { VStack{ HStack{ Text("Name") .fontWeight(.thin) Text(person.name)
Spacer() } HStack{ Text("Sex") .fontWeight(.thin) Text(person.sex)
Spacer() } } }
}
// Add/Edit View of Individual Personnel Informationstruct PersonEditView: View {
@Environment(\.dismiss) var dismiss
@EnvironmentObject var personVm:PersonViewModel
@State var name:String = "" @State var sex:String = "Man"
var body: some View { Form { TextField("Please enter person's name", text: $name)
Picker("Sex of personnel", selection: $sex) { Text("Man") .tag("Man") Text("Woman") .tag("Woman") }
} .navigationTitle("\(personVm.selected == nil ? "Add" : "Edit") Personnel") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarLeading) { Button("Cancel") { dismiss() } }
ToolbarItem(placement: .topBarTrailing) { Button("Save") { var isUpdate = true if personVm.selected == nil { isUpdate = false personVm.selected = Person() } personVm.selected?.name = name personVm.selected?.sex = sex
if isUpdate { personVm.updatePerson() }else{ personVm.insertPerson() }
dismiss()
} } } .onAppear { if let person = personVm.selected { self.name = person.name self.sex = person.sex } } }
}
전체 코드
import SwiftUI
// MARK: Modelstruct Person:Identifiable { var id = UUID() var name:String = "" var sex:String = ""}
// MARK: ViewModelclass PersonViewModel:ObservableObject {
// Person List @Published var persons:[Person] = []
@Published var selected:Person?
// Query Person List func fetchList(){ // Here it is assumed that you get the data from the server or a source such as CoreData and assign it to self.persons }
// Insert Person func insertPerson(){ // Simulation of additional operations if let person = selected { self.persons.append(person) }
}
// Update Person func updatePerson(){ // Simulation of modification operations if let person = selected, let index = persons.firstIndex(where: { $0.id == person.id }) { persons[index] = person } }
// Delete Person func deletePerson(offsets: IndexSet){ // Here it is only deleted from the page list, if the data is still stored elsewhere, then it needs to be deleted together with the new related logic offsets.forEach { persons.remove(at: $0) } }
}
// List Viewstruct SwiftUIMVVM: View {
@StateObject var personVm:PersonViewModel = PersonViewModel()
@State var showEdit = false
var body: some View {
NavigationStack { List { ForEach(personVm.persons) { person in Button(action: { personVm.selected = person showEdit = true }, label: { personView(person: person) })
} .onDelete(perform: { indexSet in personVm.deletePerson(offsets: indexSet) }) } .listRowSpacing(10) .toolbar { ToolbarItem(placement: .topBarTrailing) { Button("", systemImage: "plus") { showEdit = true } } } .sheet(isPresented: $showEdit, onDismiss: { personVm.selected = nil }, content: { NavigationStack { PersonEditView() .environmentObject(personVm) } .presentationDetents([.medium])
}) }
}
// Display list information for each person // Here we have abstracted the code to increase its readability func personView(person:Person) -> some View { VStack{ HStack{ Text("Name") .fontWeight(.thin) Text(person.name)
Spacer() } HStack{ Text("Sex") .fontWeight(.thin) Text(person.sex)
Spacer() } } }
}
// Add/Edit View of Individual Personnel Informationstruct PersonEditView: View {
@Environment(\.dismiss) var dismiss
@EnvironmentObject var personVm:PersonViewModel
@State var name:String = "" @State var sex:String = "Man"
var body: some View { Form { TextField("Please enter person's name", text: $name)
Picker("Sex of personnel", selection: $sex) { Text("Man") .tag("Man") Text("Woman") .tag("Woman") }
} .navigationTitle("\(personVm.selected == nil ? "Add" : "Edit") Personnel") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarLeading) { Button("Cancel") { dismiss() } }
ToolbarItem(placement: .topBarTrailing) { Button("Save") { var isUpdate = true if personVm.selected == nil { isUpdate = false personVm.selected = Person() } personVm.selected?.name = name personVm.selected?.sex = sex
if isUpdate { personVm.updatePerson() }else{ personVm.insertPerson() }
dismiss()
} } } .onAppear { if let person = personVm.selected { self.name = person.name self.sex = person.sex } } }
}
#Preview { SwiftUIMVVM()}
요약
MVVM
은 예제에서 다음과 같이 구성됩니다:
M(Model):
Person
은 데이터 구조를 담당하며 뷰 계층과 완전히 분리되어 있습니다.
V(View):View
프로토콜을 구현한SwiftUIMVVM
및PersonEditView
는 데이터를 표시하는 역할을 합니다.
VM(ViewModel):ObservableObject
프로토콜을 구현한PersonViewModel
은 데이터를 관리하고 비즈니스 로직을 담당합니다.
확장
코드에서 사용된 주요 개념들에 대해 간단히 설명하겠습니다.
1. ObservableObject
ObservableObject
는 프로토콜로, 이를 준수하는 클래스는 속성의 변경을 SwiftUI의 뷰에 게시하여 데이터 변경 시 뷰가 자동으로 다시 렌더링되도록 할 수 있습니다.
- 작동 방식:
ObservableObject
를 따르는 클래스는 속성을 게시함으로써(일반적으로@Published
를 사용하여 표시) 이 클래스에 의존하는 뷰에 알림을 보냅니다. 이러한 속성이 변경되면 SwiftUI는 자동으로 뷰를 다시 렌더링합니다. - 사용 시나리오: 일반적으로 MVVM 패턴에서
ViewModel
에 사용됩니다.ViewModel
은 데이터를 관리하고 비즈니스 로직을 담당하며,ObservableObject
는 이러한 변경 사항이 뷰에 실시간으로 반영되도록 보장합니다.
2. @Published
@Published
는 ObservableObject
의 속성을 표시하는 데 사용되는 속성 래퍼로, 속성 값이 변경될 때 이를 의존하는 뷰에 업데이트를 알리는 역할을 합니다.
- 작동 원리:
@Published
속성의 값이 변경될 때,ObservableObject
는 자동으로 모든 청취 중인 뷰에 업데이트 알림을 발송하고, 뷰는 새 데이터를 기반으로 다시 렌더링됩니다. - 사용 시나리오: 데이터 변경 시 UI 업데이트가 자동으로 이루어져야 하는 경우, 주로 비즈니스 로직 또는 데이터 처리를 담당하는
ViewModel
에서 사용됩니다.
3. @EnvironmentObject
@EnvironmentObject
는 SwiftUI에서 제공하는 특수한 속성 래퍼로, 전역 ObservableObject
인스턴스를 앱 내 여러 뷰에서 공유할 수 있도록 해줍니다.
- 작동 원리: 부모 뷰에서
environmentObject(_:)
를 사용하여 객체를 환경에 주입하면, 자식 뷰는@EnvironmentObject
속성 래퍼를 통해 이 공유 객체에 접근할 수 있습니다. 이는 데이터 전송을 직접적으로 하지 않고도 여러 뷰에서 상태를 공유할 수 있어 유용합니다. - 사용 시나리오: 애플리케이션의 여러 뷰 사이에서 전역 상태를 공유할 때 사용됩니다. 예를 들어, 애플리케이션의 여러 페이지에서 사용자 데이터, 설정 또는 기타 전역 상태를 공유할 때 적합합니다.