The UI architectural design patterns and best practices used to organise iOS code into logical components evolved over the last years. Most of the times developers prefer to avoid the Model View Controller (MVC) pattern in favour of cleaner, modular and more testable patterns. You might be already familiar with MVP, MVVM, VIPER, MVI, etc. Like any tool, all of them have pros and cons and should be used on a case by case basis.
In this article, I’ll show how to build an iOS application that follows MVVM Design Pattern and uses Combine framework under the hood.
What is MVVM?
Model-View-ViewModel (MVVM) is a UI architectural design pattern that decouples UI code from the business and presentation logic of an application. As it comes from the name, MVVM divides an application into three components to help separate the code: the model, the view, and the view model. Let’s discuss the purpose of each of those.
- The View defines the layout, appearance and structure of the UI. The view informs the ViewModel about user interactions and observables state changes exposed by the viewModel.
- The ViewModel is responsible for wrapping the model and providing state to the UI components. It also defines actions, that can be used by the view to pass events to the model. However, it shouldn’t have access to the view.
- The Model defines core types and implements application business logic. It is completely independent of the view and view-model and reusable in many across the application.
Let’s dive into details and have a look at how can we implement an application that follows this pattern. We’ll create an iOS application that uses TMDb API to search a movie and show the details.
As with any design pattern, there are many ways to implement MVVM in Swift. In this article, I’ll follow the SOLID design principles and keep the focus on having clean, maintainable and testable code.
As it was mentioned above, the model layer consists of the model objects and use cases that encapsulate the data and behavior of the application. The use cases are typically utilized in conjunction with services that contain data access and caching. Taking it all into account, we can declare the
As you can see, the protocol functions are quite straight-forward. All of them return a type-erasing publisher, that can deliver a sequence of values over time. We’re now ready to implement the
MoviesUseCase class consumes network and image loader service via initializer. Those are responsible for fetching data via network and image loading and caching. The
searchMovies function could be implemented as following using Combine framework:
load creates a publisher that delivers the results of performing URL session data tasks. It returns down the pipeline
Result<Movies, NetworkError> object.
➋ The map operator is used to transform the result object.
➌ Performs the work on the background queue.
➍ Switches to receive the result on the main queue.
eraseToAnyPublisher does type erasure on the chain of operators so the
searchMovies(with:) function returns an object of type
AnyPublisher<Result<[Movie], Error>, Never>.
With the above-mentioned code in place, we’re now ready to declare viewModel for the search screen. You might consider several options at this point. It should be a nice idea to expose
@Published properties in the viewModel and observe changes from the view. A better solution would be defining a ViewModel, that transforms the input to the output:
MoviesSearchViewModelInput is a struct that defines UI events to be used by the viewModel:
MoviesSearchViewModelOuput defines the view’s state via the type-erasing publisher:
It should be pointed out that you could have more complex output type in a real project. It can be declared as a struct then.
Next, we have to declare the
MoviesSearchViewModel class. It is initialized with
MoviesSearchNavigator objects, that define movies search business rules and screens navigation respectively.
We’re now ready to implement the transform function. This is the most important and probably complex part of our project:
➊ Cancels current subscriptions.
➋ Adds a subscriber to show the details screen when a user taps on a movie from the list.
➌ Debounces search events and removes duplicates to create the
➍ The creation of the
movies publisher, that starts search on user input and emits
MoviesSearchState objects eventually.
idle state publisher, that emits value immediately(default state) and when the search string is empty.
movies state publishers. Calls
eraseToAnyPublisher that does type erasure on the chain of operators so the
transform(input:) function returns an object of type
Using the above setup we can implement the
MoviesSearchViewController. It consumes a
MoviesSearchViewModelType instance via initializer and binds one on
Next, we need a way to declare UI events. This could be achieved with
PassthroughSubject type, that provides a convenient way to adapt existing imperative code to the Combine model:
We can use these events to declare the
bind function which is called from
viewDidLoad. It establishes a binding with the viewModel, subscribes on the output(state) changes and renders one when changed:
Just like that, we’ve created the movies search screen that follows MVVM software design pattern and is built with Combine framework.
The Model-View-ViewModel pattern helps to neatly separate the application logic and UI. It results in having single-purpose components that are easier to test, maintain, and evolve. MVVM works greatly in conjunction with functional reactive frameworks like Combine, that encourage you to write clean, readable code.
Thanks for reading!