Skip to content

从一个示例了解SwiftUI的MVVM模式

示例图片

SwiftUI中,MVVM(Model-View-ViewModel)模式是推荐的设计模式之一,因为它很好地适应了SwiftUI的声明式编程风格。通过MVVM,可以更好地管理数据流和UI更新,同时保持代码的模块化和可维护性。

分步示例

首先定义一个Model,用来表示数据结构

// MARK: Model
struct Person:Identifiable {
var id = UUID()
var name:String = ""
var sex:String = ""
}

然后定义一个可触发页面响应的ViewModel,这个示例里面模拟了对数据的增删改查

// MARK: ViewModel
class 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: Model
struct Person:Identifiable {
var id = UUID()
var name:String = ""
var sex:String = ""
}
// MARK: ViewModel
class 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协议的SwiftUIMVVMPersonEditView视图,负责展示数据
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
@EnvironmentObjectSwiftUI中的一种特殊的属性包装器,它允许我们在应用程序中注入一个全局的 ObservableObject 实例,并在多个视图中共享它,而不需要通过每个视图手动传递。

  • 工作原理:我们可以在某个父视图中使用 environmentObject(_:) 将一个对象注入环境,子视图通过 @EnvironmentObject 属性包装器来访问这个共享的对象。它特别适合用来在多层级视图之间共享状态,而不需要繁琐地手动传递数据。

  • 使用场景:用于在应用程序的不同视图之间共享全局状态。例如,在一个应用的多个页面共享用户数据、设置或其他全局状态时特别有用。