Skip to content

Starting the MVVM pattern for SwiftUI with an example

Image

In SwiftUI, the MVVM (Model-View-ViewModel) pattern is one of the recommended design patterns as it fits well into the declarative programming style of SwiftUI. With MVVM, data flow and UI updates can be better managed while keeping the code modular and maintainable.

Step-by-step example

First define a Model to represent the data structure

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

Then define a ViewModel that triggers a page response. This example simulates adding, deleting and retrieving data.

// MARK: ViewModel
class 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) }
}
}

Finally the view layer is constructed, associating the data with the view via a ViewModel

// List View
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])
})
}
}
// 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 Information
struct 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
}
}
}
}

Full Code

Here is the full code:

import SwiftUI
// MARK: Model
struct Person:Identifiable {
var id = UUID()
var name:String = ""
var sex:String = ""
}
// MARK: ViewModel
class 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 View
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])
})
}
}
// 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 Information
struct 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()
}

Summaries

MVVM inside our example, respectively:

M(Model): Person is responsible for the data structure and is fully decoupled from the view layer
V(View): The SwiftUIMVVM and PersonEditView views, which implement the View protocol, are responsible for displaying data
VM:(ViewModel): A PersonViewModel that implements the ObservableObject protocol and is responsible for managing data and business logic.

Extensions

A brief mention of the knowledge points we use inside the code
1. ObservableObject
An ObservableObject is a protocol by which a class that adheres to it can publish changes to its properties to a SwiftUI view, enabling the view to automatically re-render itself when the data changes.

  • How it works: a class that follows ObservableObject notifies views that depend on this class by publishing the properties (usually using the @Published tag). When these properties change, SwiftUI automatically re-renders the view.
  • Usage Scenario: Typically used for ViewModel, which is responsible for managing data and business logic in the MVVM model. The ObservableObject ensures that these changes are captured by the view in real time.

2. @Published
@Published is a property wrapper that marks a property in ObservableObject to indicate that any view that depends on this property is notified of updates when the value of this property changes.

  • How it works: When the value of a @Published property changes, ObservableObject automatically publishes an update notification to all listening views, which re-render based on the new data.

  • Usage Scenario: Where you need to be automatically notified of UI updates when data changes, such as commonly used when dealing with business logic or data operations in ViewModel.

3. @EnvironmentObject
@EnvironmentObject is a special kind of property wrapper in SwiftUI that allows us to inject a global ObservableObject instance into an App and share it across multiple views without having to manually pass it through each view.

  • How it works: We can inject an object into the environment using environmentObject(_:) in one of the parent views, and the child view accesses this shared object via the @EnvironmentObject property wrapper. It’s particularly good for sharing state between multiple hierarchical views without the need to tediously pass data manually.

  • Usage Scenario: Used to share global state between different views of an App. For example, it is especially useful when sharing user data, settings, or other global state across multiple pages of an App.