Introduction to MVVM with SwiftUI and Combine

Sreehari M Nambiar
The Startup
Published in
5 min readMay 19, 2020

Over many years iOS Engineers have explored and experimented different architectural styles like MVC, MVVM, VIP, VIPER and many more. After 11 long years, Apple have decided to move away from an Event-Driven, Imperative UIKit to a State-Driven, Declarative SwiftUI. With SwiftUI’s State driven characteristic, along with Reactive Combine framework, MVVM fits in naturally as an architectural pattern.

Core components in MVVM

  • Model: It represents your domain model and contains all the business logic. It will not have any knowledge on how the view would be presented to user.
  • View: This is passive and doesn’t have any knowledge on business. It’s just a visual representation of ViewModel.
  • View Model: It represents a State of the View at any given point of time. It will also contain the Presentation logic. ViewModel transforms the Model in a way that view can consume directly. When there’s a change in the model, ViewModel informs View about the change, mostly through binding.

Let’s take a simple example of Stopwatch to architect our SwiftUI app with MVVM.

UI consist of:

  1. Text to show Time Elapsed.
  2. Below the Text we have two buttons. One to Start/Stop the Stopwatch and the other to record the Lap time.
  3. We also have a List to display all the recorded Lap times.

Let’s take a closer look at the Model

We have a simple model to publish the time elapsed to interested parties .

Some Foundation types have already exposed the publisher functionality for Combine. Timer being one of those, we can invoke Timer.publish(every: 1.0, on: RunLoop.main, in: .default) to get a Timer Publisher.

Timer is a Connectable Publisher, which means, Subscriber has to connect manually to start receiving published values. We can also invoke autoconnect on the publisher to let the subscriber start receiving values as soon as it is connected.

We use a built-in subscriber called Sink to receive values over the time. Sink returns AnyCancellable, a type erased Cancellable, which we keep a reference to, so that we can cancel the subscription upon Stop message.

When Timer is fired, we increment timeElapsed by 1. Note that we have marked our timeElapsed with @Published property wrapper.

@Published is a useful Property Wrapper to make a property a Publisher . So whenever we update its value, the update is published to all its subscribers.

ViewModel

Our ViewModel conforms to ObservableObject. Anything that conforms to ObservableObject can be used inside SwiftUI and be subscribed.

  • elapsedTime publishes the elapsed time. Note that the type is String so that view can directly use its value without any transformation.
  • laps publishes the recorded lap times
  • buttonText publishes the text of Start/Stop button
  • isLapButtonDisabled publishes whether or not Lap Button is enabled. Lap button should be disabled when StopWatch is not running.
  • cancellable we also keep a reference to the Cancellable returned while subscribing the model publisher. This is needed to cleanup later.
  • clock ViewModel will have a reference to the Model

View can subscribe to these Publisher properties and get notified whenever its values change.

In init method, we subscribe timeElapsed publisher in the model so that we get notified when there’s a change. Since timeElapsed publisher publishes Int, we need to transform the type to String with map operator. We then use assign Subscriber to assign it our elapsedTime property. Since elapsedTime is a publisher, it immediately publishes the new value to its subscribers.

Like Sink, assign also returns AnyCancellable. We need to keep a reference to this to do any cleanup at a later point.

When ViewModel receives a buttonTapped message, we update our state to either Running or Not Running based on current state. When ViewModel receives a lap message, we simply append the current elapsed time to the laps property.

If new state is Running, Update button text to “Stop”, enable lap button and ask clock to start. Otherwise update button text to “Start”, disable lap button and ask clock to stop.

View

View has become a mere representation of ViewModel. View has an @ObservedObject ViewModel property. So whenever there is a change in any property inside ViewModel, View is updated to match the current state.

  • At the top level we have a Navigation view which embeds a VStack.
  • First item inside VStack is a Text representing elapsed time. It’s value is bound to ViewModels’s elapsedTime.
  • Then we have a HStack to show Start/Stop button and Lap button. Button actions are simply forwarded to ViewModel instead of View managing the current State.
  • Then we have a List whose items are bound to ViewModel’s “lap” property.

Conclusion

Even though Mac development environment provided a two-way binding mechanism through KVO, iOS never had one such mechanism until the release of Combine framework. With the introduction of Combine and SwiftUI we do not have to integrate libraries like RxSwift or ReactiveCocoa to get started with reactive programming. It’s a lot simpler now and easy to reason about the code.

This might be a simple Use-Case to understand the whole of Combine and SwiftUI but this could be a starting point to get your hands dirty on Combine. In the next article we’ll look at how to leverage the power of dependency injection to create a more testable and agile architecture.

Find the complete source code here.

--

--

The Startup
The Startup

Published in The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +772K followers.

Sreehari M Nambiar
Sreehari M Nambiar