One of the standout features of SwiftUI is its reactive paradigm, which seamlessly updates views as data changes. This powerful approach isn’t confined to SwiftUI alone; it can also be harnessed in UIKit through the use of the Combine framework, and starting with iOS 17, through the new Observation framework.

In this post, we’re going to explore using MVVM with UIKit, leveraging Combine to achieve reactive behaviors similar to SwiftUI. I hope you find this information useful and that it helps you improve the way you create and think about your applications. Join me as we dive into making your UIKit apps more responsive and robust.

What is MVVM?#

MVVM (Model-View-ViewModel) is a popular architectural pattern that divides the application into three interconnected components. This separation aids in managing complexities and enhances the maintainability of the application.

  • The Model includes both the data and the business logic specific to the application. It represents the actual data content and the operations that can manipulate this data.
  • The ViewModel acts as an intermediary between the Model and the View. It handles most of the view logic, transforming data from the Model in a way that the View can easily present. The ViewModel also responds to user input and view-related logic, thus abstracting the View from the Model. A key component of the ViewModel is its reactivity; it notifies the View of changes to the data automatically, facilitating a dynamic user experience.
  • The View is responsible for presenting the data provided by the ViewModel in a user-friendly format. It focuses purely on the visual representation, relying on the ViewModel to manage any state or processing of data. The View interacts with the ViewModel through mechanisms like data binding, which ensures that changes in the ViewModel are automatically reflected in the View.

By separating concerns in this manner, MVVM facilitates a clear division of labor with respect to how data is managed, presented, and interacted with. This not only makes the application easier to maintain but also enhances its scalability and testability.

How to Obtain Reactivity?#

To ensure that view controllers are aware of changes in the ViewModels, a mechanism for subscribing to and receiving notifications of these changes is necessary. One effective approach to achieve this in iOS development is through the use of Apple’s Combine framework.

Combine allows ViewModels to publish updates whenever their state changes. View controllers can subscribe to these publications and react accordingly. This setup ensures that the View layer remains responsive and up-to-date with the latest data. Here’s a basic example of how this might be implemented:

  • ViewModel publishes changes: The ViewModel includes properties marked with @Published, triggering the publisher whenever the property’s value changes.
  • View controller subscribes to changes: In the view controller, a subscriber is set up to listen to the ViewModel’s publishers. Using Combine’s sink(receiveValue:) method, the view controller can update its UI components whenever it receives new data.

This model promotes a reactive and efficient architecture where data flow and user interface updates are tightly coordinated.

Making an app: MVVM Counter#

Let’s recreate a simple counter app using MVVM + Combine + UIKit. This app is a great first introduction to reactive programming since it shows the basic components and their interactions.

Where is the model?#

This app is quite simple, the model is just an Int that will be updated every time we press the button. That’s why we aren’t creating an explicit model object. But remember with more complex models is a good idea to have a separate struct for them.

Creating the View Model#

The View Model will contains the state and the methods to interact with the states, this object will relay on Combine to publish the changes to their subscribers.

Create a new file called ViewModel.swift and add the following code.

import Combine

class ViewModel: ObservableObject {
    @Published private(set) var counter = 0
    
    func addOne() {
        counter += 1
    }
}

Here is a brief description of the code:

  • class ViewModel: ObservableObject: We define a class ViewModel that conforms to the ObservableObject protocol. This protocol is part of the Combine framework and allows the properties of our ViewModel to be observed by the UI for changes.

  • @Published private(set) var counter = 0: Here, counter is a property that stores a numerical value, initialized to 0. The @Published attribute before counter makes it a published property, meaning any UI components observing this property can react to changes in its value. The private(set) modifier means that the value of counter can only be modified within the ViewModel class itself, enforcing encapsulation and control over how the value is changed.

  • func addOne(): This function defines the behavior to increment the counter by 1. Whenever addOne is called, the counter property is increased, and because it’s a @Published property, all observers (such as UI components) are notified of this change.

Creating the View#

To create the View we are using a UIViewController, let’s create a new file and call it ViewController.swift.

First, let’s add the UI Elements to our controller. In this simple app we only need a UILabel and a UIButton.

import UIKit

class ViewController: UIViewController {
    
    private lazy var counterLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = "Button pressed 0"
        return label
    }()
    
    private lazy var counterButton: UIButton = {
        let button = UIButton()
        button.translatesAutoresizingMaskIntoConstraints = false
        button.configuration = UIButton.Configuration.filled()
        button.setTitle("Count", for: .normal)
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        configuration()
        addObjectsToView()
        addConstraintsToObjects()
        
    }
}

private extension ViewController {
    func configuration() {
        view.backgroundColor = .systemBackground
        
        counterButton.addAction(UIAction { [weak self] _ in print("Button tapped") }, for: .touchUpInside)
    }
    
    func addObjectsToView() {
        [counterLabel, counterButton].forEach {
            view.addSubview($0)
        }
    }
    
    func addConstraintsToObjects() {
        NSLayoutConstraint.activate([
            counterLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            counterLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            counterButton.topAnchor.constraint(equalTo: counterLabel.bottomAnchor, constant: 32),
            counterButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -32),
            counterButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 32)
        ])
    }
}

So far nothing new, just a simple ViewController with some UI elements.

Adding Reactivity#

Now comes the interesting part of the MVVM pattern, reacting to changes in the ViewModel.

The first step it to include Combine in the ViewController.

Next, let add a dependency to the ViewModel so that our ViewController can subscribe to the changes published by the view model.

Inside the ViewController class add the following properties

private let viewModel = ViewModel()
private var cancellables: Set<AnyCancellable> = []

The above code may become confusing because of the cancellables property, this is used to avoid memory leaks.

When you use the Combine framework to handle data flows in your applications, you often set up subscriptions where your UI components or other parts of your app listen to changes in your data. These subscriptions create what are known as “cancellables,” which are objects that represent the life of a subscription. To manage these effectively and avoid memory leaks, we use AnyCancellable.

By adding these AnyCancellable instances to a set called cancellables, you keep a reference to each active subscription. Storing them in a set is convenient because it ensures that each subscription is kept alive as long as you need it and is automatically cancelled and removed from memory when no longer needed or when the containing object (like a view model or view controller) is deallocated.

Next we need to subscribe to the published property of the view model.

Go to the configuration function and add the following at the bottom.

viewModel.$counter
            .sink { [weak self] value in
                self?.counterLabel.text = "Button Pressed \(value)"
            }.store(in: &cancellables)

The code snippet demonstrates how to update a user interface component in response to changes in a ViewModel property using Combine. Here’s how it works:

  • viewModel.$counter: This line begins observing changes to the counter property in the ViewModel. The $ prefix is a shorthand in Combine that provides a publisher for the property, which emits a new value each time the property changes.

  • .sink: The sink method is used to subscribe to the publisher and handle new data as it arrives. This method takes a closure that defines what to do with the emitted values. In this case, whenever counter updates, the closure receives the new counter value.

  • [weak self] value in: The closure captures self weakly to prevent retain cycles, which can lead to memory leaks. value is the latest value of counter emitted by the publisher.

  • self?.counterLabel.text = “Button Pressed (value)”: Inside the closure, the text of counterLabel (a UI label) is updated to display the number of times a button has been pressed, as indicated by the counter value.

  • .store(in: &cancellables): This part of the code is crucial for managing the subscription’s lifecycle. It adds the subscription to a set of cancellables (explained in the previous part about AnyCancellable). This ensures that the subscription remains active as long as needed and gets properly disposed of to prevent memory leaks.

This pattern is commonly used in iOS development with UIKit to dynamically update the UI in response to data changes in the ViewModel, enhancing the app’s reactivity and user experience.

Finally let’s update the button action to call the method that will triggers the View Model updates, in the configuration method, change the button action to the following.

counterButton.addAction(UIAction { [weak self] _ in self?.viewModel.addOne() }, for: .touchUpInside)

This will change the value in the view model each time we press the button, by changing the value we will trigger the notification in the published property, making the view controller to react to the change and update the value in the label automatically.

Here is the final form of the ViewController.

import UIKit
import Combine

class ViewController: UIViewController {
    
    private let viewModel = ViewModel()
    private var cancellables: Set<AnyCancellable> = []
    
    private lazy var counterLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = "Button pressed 0"
        return label
    }()
    
    private lazy var counterButton: UIButton = {
        let button = UIButton()
        button.translatesAutoresizingMaskIntoConstraints = false
        button.configuration = UIButton.Configuration.filled()
        button.setTitle("Count", for: .normal)
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        configuration()
        addObjectsToView()
        addConstraintsToObjects()
        
    }
}

private extension ViewController {
    func configuration() {
        view.backgroundColor = .systemBackground
        
        counterButton.addAction(UIAction { [weak self] _ in self?.viewModel.addOne() }, for: .touchUpInside)
        
        viewModel.$counter
            .sink { [weak self] value in
                self?.counterLabel.text = "Button Pressed \(value)"
            }.store(in: &cancellables)
    }
    
    func addObjectsToView() {
        [counterLabel, counterButton].forEach {
            view.addSubview($0)
        }
    }
    
    func addConstraintsToObjects() {
        NSLayoutConstraint.activate([
            counterLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            counterLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            counterButton.topAnchor.constraint(equalTo: counterLabel.bottomAnchor, constant: 32),
            counterButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -32),
            counterButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 32)
        ])
    }
}

Running the app#

As simple as this you’ve implemented MVVM with Combine and UIKit. This example is a great way to observe the simplicity and power of reactive programming.

Here is the final result.

mvvm-combine-uikit

Conclusion#

Throughout this post, we’ve explored how the MVVM architectural pattern can be effectively implemented in UIKit applications using the Combine framework to create reactive behaviors similar to those found in SwiftUI. By integrating MVVM with Combine, we’ve demonstrated that UIKit is far from obsolete in the era of modern, reactive iOS applications. Instead, it offers a robust platform for adopting reactive programming paradigms that can make your apps more responsive and easier to maintain.

The MVVM pattern not only separates concerns, enhancing the maintainability and testability of your applications, but also leverages the power of Combine to keep your UI in perfect sync with the underlying data model. As we have seen with our simple counter app example, even traditional UIKit apps can benefit immensely from the clarity and efficiency that reactive programming brings.

I hope this walkthrough has provided you with a clear understanding and practical knowledge on integrating MVVM with UIKit using Combine, and inspires you to apply these concepts to your own iOS projects. Reactive programming might seem daunting at first, but with practice, it becomes an indispensable part of crafting elegant and efficient iOS applications.

Thank you for joining me on this journey through reactive programming with UIKit and MVVM. Happy coding!