MVVM in SwiftUI - Part 2 | Implementation

MVVM in SwiftUI - Part 2 | Implementation
Photo by David Schultz / Unsplash

We will be making a sample application that provide definitions for a given word.

struct ContentView: View {
    @State private var searchText: String = ""
    @State private var definition: String = ""
    
    private func searchDefinition() {
        self.definition = self.searchText
    }
    
    var body: some View {
        VStack(spacing: 20) {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text("Search for Word Definitions")
            HStack {
                TextField("Enter your word here", text: self.$searchText)
                    .textFieldStyle(.roundedBorder)
                Button(action: {
                    self.searchDefinition()
                }, label: {
                    Image(systemName: "magnifyingglass")
                })
            }
            if self.definition != "" {
                Text(definition)
                    .padding(.vertical, 50)
            }
            Spacer()
        }
        .padding()
    }
}

This code defines a SwiftUI view that displays an interface for searching and displaying the definition of a word. The view has an image, a text label, a text field for entering the search term, and a button for initiating the search. When the search button is pressed, the view updates to display the definition of the searched word (which is hardcoded in this example, but would normally come from a server or database). The @State property wrapper is used to manage the search term and the displayed definition. Whenever the user enters a new search term, the view updates automatically to reflect the new value.

There are a few problems with this code from an MVVM perspective:

  1. All the logic for searching and displaying the definition of a word is contained in the View. In MVVM, the View should only be responsible for rendering the UI, while the ViewModel handles business logic and data manipulation. This makes the code easier to maintain and test.
  2. The View is directly invoking a method on itself to initiate the search, which tightly couples the UI to the search logic. In MVVM, user actions should be handled by the ViewModel, which then updates the state of the app accordingly. This separation of concerns makes it easier to modify the app's behaviour without affecting the UI.

To adhere more closely to the MVVM pattern, the code could be refactored to move the search logic into a separate ViewModel, which would handle communication with the model and publish the results for the View to display. The View would then be responsible only for rendering the UI and responding to user input, while the ViewModel would handle business logic and data management.

ViewModel Implementation

@MainActor
class ContentViewModel: ObservableObject {
    @Published var searchText: String = ""
    @Published var definition: String = ""
    
    public func searchDefinition() {
        self.definition = self.searchText
    }
}

In SwiftUI, an ObservableObject is a protocol that defines an object that can be observed for changes. When an object conforms to the ObservableObject protocol, it can be used as a view model in a SwiftUI app. The view can then observe the changes to the view model's properties and update the UI accordingly.

In terms of MVVM, the ContentViewModel class serves as the ViewModel layer. It encapsulates the state and business logic of the view, such as handling user input and fetching data.

The @Published properties in the ViewModel represent the state of the view, and are used to communicate changes to the View layer.

The searchDefinition() method represents the business logic of the ViewModel, which is responsible for performing any necessary operations based on the user's input.

The @MainActor decorator is used to indicate that a class or method should only be executed on the main thread (also known as the main dispatch queue). It's a new feature in Swift 5.5 that helps to ensure the safety and correctness of concurrent code. In this specific code snippet, the @MainActor decorator is used to ensure that the ContentViewModel class is only accessed and modified on the main thread.

Refactoring the View to use the ViewModel

In SwiftUI, you typically initialise a ViewModel in the @StateObject or @ObservedObject property wrapper of the View that uses it. The @StateObject property wrapper is used when you want to create a new instance of the ViewModel each time the View is re-created. This is useful for Views that are created dynamically, such as in a ForEach loop. The @ObservedObject property wrapper, on the other hand, is used when you want to reuse an existing instance of the ViewModel across multiple Views. Refactored view that uses the ViewModel will be as follows.

struct ContentView: View {
    @StateObject private var viewModel: ContentViewModel = ContentViewModel()
    
    var body: some View {
        VStack(spacing: 20) {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text("Search for Word Definitions")
            HStack {
                TextField("Enter your word here", text: self.$viewModel.searchText)
                    .textFieldStyle(.roundedBorder)
                Button(action: {
                    self.viewModel.searchDefinition()
                }, label: {
                    Image(systemName: "magnifyingglass")
                })
            }
            if self.viewModel.definition != "" {
                Text(self.viewModel.definition)
                    .padding(.vertical, 50)
            }
            Spacer()
        }
        .padding()
    }
}

Implementing the Model layer

In this example, the model layer could be a separate data source, such as a database or a remote API, that provides the definitions for the search terms entered by the user. The ViewModel layer would then interact with the model layer to fetch the data and update the view accordingly.

In this example, the model layer could be a separate data source, such as a database or a remote API, that provides the definitions for the search terms entered by the user. The ViewModel layer would then interact with the model layer to fetch the data and update the view accordingly.

In MVVM, the ViewModel layer interacts with the Model layer to fetch and process data required to update the View. This class represents the model layer of the MVVM architecture. Its responsibility is to provide data to the ContentViewModel that is required to update the View. Note that the getDefinition function is async because in real world use-cases retrieving data will almost always will be asynchronous.

We can refractor the ViewModel code to use this Model layer as follows,

@MainActor
class ContentViewModel: ObservableObject {
    private var model = ContentModel()
    @Published var searchText: String = ""
    @Published var definition: String = ""
    
    public func searchDefinition() {
        Task {
            let response = await model.getDefinition(forWord: self.searchText)
            self.definition = response
        }
    }
}

In this blog, we discussed how to implement the MVVM architecture pattern in a SwiftUI app. By separating the concerns of the app into distinct components, we were able to create a more maintainable and testable codebase. We looked at how to use ObservableObjects to create a ViewModel that manages the state of the app and how to bind a View to the ViewModel using @ObservedObject. We also covered how to handle user input and asynchronous operations. In the next blog we will dive deeper into handling use-cases.