Using ViewModel with Protocols in SwiftUI

Alejandro Zalazar
5 min readFeb 13, 2024

--

LEGO building experience transferred into the iOS app design

In this article we’ll explore how to use generics with protocols to apply MVVM pattern in SwiftUI.

Disclaimer

This article doesn’t in any way mark MVVM as the default pattern to be used with SwiftUI; it is merely a guide where you try to explain how to use it with generics.

Requisites

Difficulty: Beginner | Easy | Normal | Challenging

This article has been developed using Xcode 15.0, and Swift 5.9 without Observation framework.

About generics

Generics in Swift are not seen as often as in other languages; however, generics are extremely powerful in writing flexible and reusable functions throughout the base code.

General Idea

When you work with generics, you will want to define a protocol to which your view models will fit; this may get the name that is relevant to your case of use, but for this example we will simply name our protocol with the nomenclature convention of our view model + “Protocol”.

Step 1: Define the View Model Protocol

Start by defining the view model protocol that outlines the properties and methods that your views will interact with. This protocol serves as the contract that your view models will conform to.

protocol CounterViewModelProtocol: ObservableObject {
var count: Int { get set }
func didTapIncrement()
func didTapDecrement()
}

Since we are going to use our protocol as a state object, the protocol itself must match @ObservableObject. Now, all of our vision model implementations can fit this protocol and allow us to use the protocol as generic in our opinion.

Step 2: Create a Concrete View Model

Implement a concrete class that conforms to the view model protocol. This class will provide the actual functionality and data that your views will use.

final class CounterViewModelImpl: CounterViewModelProtocol {
@Published var count: Int = 0

func didTapIncrement() {
count+= 1
}

func didTapDecrement() {
count-= 1
}
}

Step 3: Create a SwiftUI View

Now, create a SwiftUI view that uses the view model protocol. Inject the view model into the view using the @StateObject property wrapper.

struct CounterView<ViewModel>: View where ViewModel: CounterViewModelProtocol {
@StateObject private var viewModel: ViewModel

init(viewModel: @autoclosure @escaping () -> ViewModel) {
self._viewModel = StateObject(wrappedValue: viewModel())
}

var body: some View {
VStack {
Text(viewModel.count)
Button("Increment") {
viewModel.didTapIncrement()
}
Button("Decrement") {
viewModel.didTapDecrement()
}
}
}
}

Here, by using @autoclosure @escaping , we ensure we delay the ToggleViewModel 's init and hand it over to wrappedValue's autoclosure safely. — For more info

To create our view, we will use a generic ViewModel parameter in our CounterView example view. We can also define what type our generic type parameter matches, in our example will be CounterViewModelProtocol, the protocol that we just created.

Step 4: Instantiate and Use the View Model

In your main view or app entry point, instantiate the concrete view model and pass it to the SwiftUI view you created.

let viewModel = CounterViewModelImpl()
CounterView(viewModel: viewModel)

Now, whenever we create an instance of our view from somewhere else in our application, we can use CounterView(viewModel: CounterViewModelImpl()) to get the configuration of the “real” view model to be used. This also gives us a lot of flexibility, as we can now use other implementations of CounterViewModelProtocol to create more robust features in the future or, for example, use them in SwiftUI Previews.

Step 5: Adding Functionality

You can continue to build upon this foundation by adding more properties and methods to your view model protocol and its conforming implementations. You can also inject dependencies into the view model’s initializer to follow dependency injection principles.

Remember that this example demonstrates a simple case. As your app grows in complexity, you can implement more advanced features, handle user interactions, and manage more intricate view models.

Keep in mind that the steps provided here are a starting point. Depending on your app’s architecture and requirements, you might need to tailor the approach to best fit your needs.

Advantages

  1. Modularity and Testability: View model protocols allow you to define a clear contract between your views and their associated view models. This promotes modularity and separation of concerns, making your codebase more maintainable and testable. You can create mock implementations for testing purposes, which makes it easier to write unit tests.
  2. Dependency Injection: With view model protocols, you can inject dependencies into your view models through their initializers. This enables you to adhere to the principles of dependency inversion and single responsibility, leading to more flexible and loosely coupled code.
  3. Protocol-Oriented Programming: Swift promotes protocol-oriented programming, and using view model protocols aligns well with this approach. Protocols allow you to define a contract that multiple types can conform to, fostering code reuse and polymorphism.
  4. Multiple Conformances: A single view can conform to multiple view model protocols, allowing you to compose view models with different functionalities. This can help in building complex screens while keeping the concerns separate and manageable.
  5. Clear API Documentation: Well-defined view model protocols provide clear API documentation for view components. This makes it easier for developers to understand the expected behavior of each view model and its associated properties and methods.

Disadvantages

  1. Increased Complexity: Introducing view model protocols can add a layer of complexity to your codebase, especially for smaller projects or simple views. The overhead of defining protocols, conforming to them, and managing multiple protocol implementations might outweigh the benefits in some cases.
  2. Learning Curve: Developers new to Swift and SwiftUI might find it challenging to understand the concept of view model protocols, especially if they are not familiar with protocol-oriented programming. This could potentially slow down development initially.
  3. Boilerplate Code: Defining view model protocols and their conforming implementations can introduce boilerplate code, especially if the views and view models are simple. This might lead to more verbose code and reduce the benefits of using protocols.
  4. Compatibility Issues: Depending on the complexity of your app and the design patterns you use, adopting view model protocols might introduce compatibility issues, especially if your project includes legacy code that doesn’t follow the same pattern.
  5. Overengineering: In some cases, using view model protocols might be seen as overengineering, especially for smaller apps or simpler views. The added abstraction could make the codebase harder to understand and maintain if not justified by the complexity of the project.

--

--