从一个示例了解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 {
// 人员列表 @Published var persons:[Person] = []
@Published var selected:Person?
// 查询人员列表 func fetchList(){ // 这里假设你从服务器或者CoreData等渠道获取数据,然后赋值给self.persons }
// 新增人员 func insertPerson(){ // 模拟新增操作 if let person = selected { self.persons.append(person) }
}
// 修改人员信息 func updatePerson(){ // 模拟修改操作 if let person = selected, let index = persons.firstIndex(where: { $0.id == person.id }) { persons[index] = person } }
// 删除人员 func deletePerson(offsets: IndexSet){ // 这里只是从页面列表删除了,如果数据还在其他地方存储,那么需要新增相关逻辑一同删除 offsets.forEach { persons.remove(at: $0) } }
}
最后构建视图层,通过ViewModel
将数据与视图关联起来
// 列表视图struct 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])
}) }
}
// 展示每一个人员的列表信息 // 这里我们将代码抽离出来,增加了代码的可读性 func personView(person:Person) -> some View { VStack{ HStack{ Text("姓名") .fontWeight(.thin) Text(person.name)
Spacer() } HStack{ Text("性别") .fontWeight(.thin) Text(person.sex)
Spacer() } } }
}
// 单个人员信息的新增/编辑视图struct PersonEditView: View {
@Environment(\.dismiss) var dismiss
@EnvironmentObject var personVm:PersonViewModel
@State var name:String = "" @State var sex:String = "男"
var body: some View { Form { TextField("请输入人员姓名", text: $name)
Picker("人员性别", selection: $sex) { Text("男") .tag("男") Text("女") .tag("女") }
} .navigationTitle("\(personVm.selected == nil ? "新增" : "编辑")人员") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarLeading) { Button("取消") { dismiss() } }
ToolbarItem(placement: .topBarTrailing) { Button("保存") { 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 {
// 人员列表 @Published var persons:[Person] = []
@Published var selected:Person?
// 查询人员列表 func fetchList(){ // 这里假设你从服务器或者CoreData等渠道获取数据,然后赋值给self.persons }
// 新增人员 func insertPerson(){ // 模拟新增操作 if let person = selected { self.persons.append(person) }
}
// 修改人员信息 func updatePerson(){ // 模拟修改操作 if let person = selected, let index = persons.firstIndex(where: { $0.id == person.id }) { persons[index] = person } }
// 删除人员 func deletePerson(offsets: IndexSet){ // 这里只是从页面列表删除了,如果数据还在其他地方存储,那么需要新增相关逻辑一同删除 offsets.forEach { persons.remove(at: $0) } }
}
// 列表视图struct 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])
}) }
}
// 展示每一个人员的列表信息 // 这里我们将代码抽离出来,增加了代码的可读性 func personView(person:Person) -> some View { VStack{ HStack{ Text("姓名") .fontWeight(.thin) Text(person.name)
Spacer() } HStack{ Text("性别") .fontWeight(.thin) Text(person.sex)
Spacer() } } }
}
// 单个人员信息的新增/编辑视图struct PersonEditView: View {
@Environment(\.dismiss) var dismiss
@EnvironmentObject var personVm:PersonViewModel
@State var name:String = "" @State var sex:String = "男"
var body: some View { Form { TextField("请输入人员姓名", text: $name)
Picker("人员性别", selection: $sex) { Text("男") .tag("男") Text("女") .tag("女") }
} .navigationTitle("\(personVm.selected == nil ? "新增" : "编辑")人员") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarLeading) { Button("取消") { dismiss() } }
ToolbarItem(placement: .topBarTrailing) { Button("保存") { 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
会自动重新渲染视图。 - 使用场景:通常用于
ViewModel
,在MVVM
模式下,ViewModel
负责管理数据和业务逻辑。ObservableObject
确保这些变化能被视图实时捕捉到。
2. @Published
@Published
是一个属性包装器,用来标记 ObservableObject
中的属性,表示这个属性的值发生变化时,通知任何依赖这个属性的视图更新。
-
工作原理:当一个
@Published
属性的值发生变化时,ObservableObject
会自动向所有监听的视图发布更新通知,视图根据新数据重新渲染。 -
使用场景:在需要数据发生变化时,自动通知UI更新的地方,比如在
ViewModel
中处理业务逻辑或数据操作时常用。
3. @EnvironmentObject
@EnvironmentObject
是SwiftUI
中的一种特殊的属性包装器,它允许我们在应用程序中注入一个全局的 ObservableObject
实例,并在多个视图中共享它,而不需要通过每个视图手动传递。
-
工作原理:我们可以在某个父视图中使用
environmentObject(_:)
将一个对象注入环境,子视图通过@EnvironmentObject
属性包装器来访问这个共享的对象。它特别适合用来在多层级视图之间共享状态,而不需要繁琐地手动传递数据。 -
使用场景:用于在应用程序的不同视图之间共享全局状态。例如,在一个应用的多个页面共享用户数据、设置或其他全局状态时特别有用。