Why MVVM can help you improve you SwiftUI project?

I saw a lot of tutorials that don’t have a clear code structure. In this tutorial, I want to tell you what the design pattern MVVM (Model – View – View Model) is and how it can help you write great code. You will learn about the data flow in SwiftUI and how property wrappers are a major part of it. The practical example demonstrates the reorganization of your code.

https://youtu.be/zRZ2cjZY-1c

You can take advantage of MVVM and

  • write smaller views that are easy to read and maintain
  • use dependency injections to show different states of your view on the preview canvas
  • integrate unite tests.

The basic idea of Model – View – View Model

First I want to talk about how MVVM works in general before we move to the implementation details with SwiftUI.

basic mvvm

The view is where we declare how the user interface should look like. In order for the view to do this, it needs to get the data from somewhere. In MVVM the view asks for the data from the view model. The view does not own the data directly. But it also should react to changes in the data to always show the new ones. We want an automatic update of the views so that we always see the right data. There should not be a mismatch between the underlying data and what the view shows. There is only one “truth”.

In the case of user interactions, we might show different views or change the data. There are some user intents where we would need to change the underlying data. The view does not manipulate the data directly. The view model is responsible to process these tasks. The view can request changes to the data via the view model. 

The view model holds the data and is responsible for processing data changes. It can be seen as the interpreter between the model and the view.

It does not know or care where this data is used in our views. It is blind to the views (which you can see from the faces in the drawing). This is referred to as blind structured communication and helps to decouple the logic from UI code. The View itself sees the view model because it needs to pull the data from it and needs to register for change notifications so it can be re-drawn.

 

But we need to have a way of letting the view model know that something changed in our data model because the view model internally needs to also communicate to the views that something changed. These something changed notifications will cause the views to re-render.

The data flow is circular as you can see with the arrows in the drawing. If for example, the user presses a button the view model handles the changes in the data.  The underlying data is changed and we see that our view model published that something’s changed. The views receive this newly published information and will be redrawn to show the new data.

How MVVM works in SwiftUI?

The blind communication of “something changed” is how MVVM handles the data flow. In SwiftUI it is based on Combine data streams, which are implemented in property wrappers. Everything that will change during runtime will need a property wrapper to ensure that the views always reflect the correct value.

In Swift, models are mostly defined as structs because structs are value types. This helps the view model to easily know if something changed. It uses property observes for the @Published properties (willSet). If you use objects for example with realm.io or Core Data you will not see the views update correctly.

But how does the view model communicate “something changed”?  How does it publish these updates? In SwiftUI every view model should conform to ObservableObject. With this protocol, you get an objectWillChange publisher. This is a PassthroughSubject which is a special publisher in Combine. You can send new values into this data stream by calling objectWillChange.send().

The views subscribe to this publisher via a property wrapper like @ObservedObject, @StateObject, or @EnvironmentObject. Therefore, they will receive the change notification. That’s why I have these small red boxes in the image. A view has a window/ an access point to the view model.

For properties that have a @Published the objectWillChange.send() is called automatically for you. But you can call objectWillChange.send() at any time. This can help to deal with realm.io or Core Data objects.

 

@Published  properties will also get a publisher that you can access with the $ prefix.You can easily create data streams from your properties in the view model. Your views can also subscribe to these publishers with the view modifiers .onReceive()

Combine makes it possible to create a two-way binding between the model and views. We have again a circular flow between view – view model – model. This is the data flow of SwiftUI.

Declarative programming: What is the one and only “truth”

There should be only one truth. One problem that you have to solve is that you always have to sync the underlying data and the data that is displayed in your views. If your views show something different than you’re underlying data, this is not good for the user and might cause some wonky behavior. Think of editing data that has already been deleted. 

What do I mean by “the truth”? What can be part of your app’s truth?

Obviously, I already said the data like documents or data fetched from the web are one kind of truth. What is the state the user sees for the first time? An app with no data is in an empty state.

Another truth that can be out there is the user state. Is the user logged in? What kind of user account does he have? Does he have a pro account? Because if he has a pro account, you want to unlock certain features. For example, in a view, you would disable a button. The user can experience a very different state of the application.

But where would you place the logic for creating an account/login in? This is a different task than loading data from the file system. It’s a good idea to separate them into different view models.

Another example of the state is navigation. This is one of the things I guess most people are not aware of when they start with SwiftUI.  

For example, we look at a list in a navigation stack. You can have a different state if the navigation link is shown and which link is shown. These state properties are declared in a NavigationLink as isPresented.  You can have a different state to show/ hide a popover. 

The first time an app launches, the user would see the onboarding. This is the onboarding state. More sub-states would be to show the login request view.

This kind of state is a state that defines the whole application. Navigation is a state. SwiftUI is declarative and state-driven. If you want to show a different view, you have to declare what state you want to change to.

How do you show changes in the truth?  – with animations.  The reason why animations work so nicely with SwiftUI is that it’s declarative. You have one state and another state.  The only thing that you need to tell the system is to animate it.  What should be the transition between state A and state B? Set timing curve and duration.

The data flow in SwiftUI: Binding data and views together

We have now the truth. The question is since it’s only one truth: who should be the owner? Where should I place my truth?  Who should have access to my truth? How do I share the truth? We’re going to have a look at types that can have the truth.

Can a view be the owner of a truth?

For example, we have a view that is the owner of a Boolean property. The user can set this property so we can mark an item as a favorite one. Boolean is “simple” data and we use a @State property wrapper. In order to give a subview access to this truth, we can choose between simple read access or a read/write access.

A subview that declares a property as @Binding is not the owner of this property. But it gets a two-way binding to it.

Who should be the owner of a truth defined in a view model?

If you have a view model that conforms to ObservableObject you might want to share it between on view or multiple or even the whole application. If you use @StateObject the view will be the owner of the view model. Other views can get access with @ObservedObject. Depending on the level in the hierarchy you can share one instance of an ObservableObject with the whole application. A greater place for this would be the Environment.


Practical coding example: How to implement MVVM

Let me show you the example we are going to use for this tutorial. You see here a project with a list of items. You can add, move and delete items. Everything is together in ContentView.swift.

				
					import Foundation
import SwiftUI
struct ContentView: View {
   @State var items = [Item(id: UUID(), name: "first"),
                      Item(id: UUID(), name: "second"),
                      Item(id: UUID(), name: "third")]
   var body: some View {          
      NavigationView {                
        List {                    
         ForEach(todoManager.items) { item in                        
              NavigationLink(destination: Text("Destination (item.name)"), 
                             label: {  Text(item.name)  })                    
         }                   
          .onDelete(perform: { indexSet in                                             
            for index in indexSet {                                        
                   items.remove(at: index)
            }                                       
          })                                      
          .onMove(perform: { indices, newOffset in                                            
            items.move(fromOffsets: indices, toOffset: newOffset)
          })                     
       }        
       .navigationBarTitle(Text("Todo's"), displayMode: .large)        
       .toolbar(content: {            
              ToolbarItemGroup(placement: .navigationBarTrailing) {                
                   EditButton()                
                  Button(action: { items.append(Item(id: UUID(), name: "newly added"))
                        }, label: { Image(systemName: "plus")  })              
              }          
       })        
    }   
  }
}
				
			

We are running into a couple of problems here because all my code is in one file. This is usually what happens when you see tutorials because they try to make them as short as possible. They don’t want to distribute the code over multiple files because they want to see the code all at once.  Which is probably good but gives you the wrong oppression.

What I cannot do is I would want to have different previews for different states. For example one preview for a full list and one for the empty state, where no items have been added yet. In this case, I want to create a placeholder that I can edit and see directly in the preview.

I have difficulties accomplishing this because all my data and my business logic are in this one view. Everything is together and I would like to decouple my view, the business logic, and my model. The logic of delete, move and add should not be done directly by the view. In the current implementation, I cannot easily write unit tests.

Step 1: Place the model in an extra group/ file

Step 2: Create a View Model

Create a new file with a class that conforms to ObservableObject. This is the new owner of the data which is the items array. All data processing is moved from the view to this class including deleting, moving, and creating items.

I also added static functions that create instances of TodoListManager for different states: the empty state and a full state. We are going to use this in the preview.

				
					import Foundation
class TodoListManager: ObservableObject {        
   @Published var items: [Item] = []
   init(isForTest: Bool = false) {        
      if isForTest {           
       //get my data and set to my items array        
      }    
  }   
 // process model data    
  func move(indices: IndexSet, newOffset: Int) {        
      items.move(fromOffsets: indices, toOffset: newOffset)
  }        
  func addItem() {        
      items.append(Item(id: UUID(), name: "newly added"))
  }      
  func delete(at indexSet: IndexSet) {       
     for index in indexSet {
           items.remove(at: index)        
     }    
  }   
  //MARK: - helper  for dependency injection     
  static func emptyState() -> TodoListManager {        
      let manager = TodoListManager(isForTest: true)        
      manager.items = [ ]        
      return manager    
  }       
  static func fullState() -> TodoListManager {        
    let manager = TodoListManager(isForTest: true)        
    manager.items =  [Item(id: UUID(), name: "first"), Item(id: UUID(), name: "second"), Item(id: UUID(), name: "third")]        
    return manager   
  }
}
				
			

Step 3: Use the view model in the view

By replacing the array of items with the view model, we need to also replace all the code where the view access or manipulates the data.

				
					import SwiftUI 
struct TodoListView: View {    
   @ObservedObject 
   var todoManager: TodoListManager
   var body: some View {       
      NavigationView {            
        ZStack {               
          List {                    
            ForEach(todoManager.items) { item in                        
              NavigationLink(destination: Text("Destination (item.name)"),
                             label: { Text(item.name) })                                            
            }                    
            .onDelete(perform: { indexSet in                       
                  todoManager.delete(at: indexSet)             
            })                    
            .onMove(perform: { indices, newOffset in                        
                  todoManager.move(indices: indices, newOffset: newOffset)
            })               
         }                                
         if todoManager.items.count == 0 {                    
             Text("Please, start by adding items")                        
               .foregroundColor(.gray)                
         }            
       }                   
       .navigationBarTitle(Text("Todo's"), displayMode: .large)        
       .toolbar(content: {           
          ToolbarItemGroup(placement: .navigationBarTrailing) {                
                EditButton()                                
                Button(action: {  
                        todoManager.addItem()         
                }, label: {                    
                        Image(systemName: "plus")  
                })     
          }        
       })       
     }            
   }
}
				
			

Advantage of MVVM: easy to implement dependency injection in SwiftUI views and in the preview

Creating different states for the preview is now easy. We use the static function in the view model

				
					struct ContentView_Previews: PreviewProvider {    
     static var previews: some View {        
          Group {            
             TodoListView(todoManager: TodoListManager.emptyState())
               .previewDisplayName("empty state")         
             TodoListView(todoManager: TodoListManager.fullState())
              .previewDisplayName("data exists")                    
          }    
     }
}
				
			

Advantage of MVVM: easy to integrate Unit Testing

You can now test the business logic independently from the views because it is now placed in the view model.

				
					import XCTest@testable 
import ListTestProject // your project name
class ListTestProjectTests: XCTestCase {     
  func testExample() throws {        
    let manager = TodoListManager.emptyState()           
    XCTAssertTrue(manager.items.count == 0, "should start with empty list of todo's")       
    manager.addItem()
    XCTAssertTrue(manager.items.count == 1, "should have one todo afer adding")    
  }
}
				
			

Final thoughts

I hope this little demo helped you to understand MVVM. The tools that we have in SwiftUI make it really easy to implement this pattern. If you have problems organizing your code you can think of a state. What is the state my application can be in?  The different states of data/information.If you see it from the point of a UX designer. They design each of the different screens and each of them describes a different state of the user journey. This is already giving you a good idea of the different states and the different truths that you want to describe from this point.

 

It definitely takes some time to get used to this declarative / state-driven development. If you have any questions leave them in the comments below.

2 thoughts on “Why MVVM can help you improve you SwiftUI project?”

  1. Design patterns and data flow are extremely difficult topics for most beginner programmers (myself included) to digest, and your explanation is one of the best I have seen so far. I found a couple minor errors in your “before” example that prevent it from running properly … the Item structure definition must be added to ContentView.swift, “todoManager.items” should be replaced with just “items”, and there’s a backslash missing in the NavigationLink statement. The second example, after implementing MVVM, still works. But as you know, the Observation framework has been introduced and the @Observable macro is now used instead of the ObservableObject protocol and @Published property wrapper. I only point out these minor issues to save future readers some time implementing your otherwise excellent examples.

    Reply

Leave a Comment

Subscribe to My Newsletter

Want the latest iOS development trends and insights delivered to your inbox? Subscribe to our newsletter now!

Newsletter Form