Working With Static Patterns: A Swift MVVM Tutorial
Real-time data-driven apps create new challenges in the way we structure our programs, especially on mobile. In this article, Toptal Freelance iOS Developer Lucas van Dongen implements an iOS chat app, comparing the classic MVC approach with a static immutable MVVM pattern.
Real-time data-driven apps create new challenges in the way we structure our programs, especially on mobile. In this article, Toptal Freelance iOS Developer Lucas van Dongen implements an iOS chat app, comparing the classic MVC approach with a static immutable MVVM pattern.
A mobile developer and teacher, Lucas has built not only iOS apps but also back-end APIs using Swift, Objective-C, Python, Elixir, and C#.
Today we’re going to see how new technical possibilities and expectations from our users for real-time data-driven applications create new challenges in the way we structure our programs, especially our mobile applications. While this article is about iOS and Swift, many of the patterns and conclusions are equally applicable to Android and web applications.
There’s been an important evolution in how modern mobile apps work over the past few years. Thanks to more pervasive internet access and technologies like push notifications and WebSockets, the user usually is no longer the only source of runtime events—and not necessarily the most important one anymore—in many of today’s mobile apps.
Let’s take a closer look at how well two Swift design patterns each work with a modern chat application: the classic model-view-controller (MVC) pattern and a simplified immutable model-view-viewmodel pattern (MVVM, sometimes stylized “the ViewModel pattern”). Chat apps are a good example because they have many sources of data and need to update their UIs in many different ways whenever data is received.
Our Chat Application
The application we’re going to use as a guideline in this Swift MVVM tutorial is going to have most of the basic features we know from chat applications like WhatsApp. Let’s go over the features we’ll implement and compare MVVM vs MVC. The application:
- Will load the previously received chats from disk
- Will sync the existing chats over a
GET
-request with the server - Will receive push notifications when a new message is sent to the user
- Will be connected to a WebSocket once we’re in a chat screen
- Can
POST
a new message to a chat - Will show an in-app notification when a new message is received of a chat we’re not currently in
- Will show a new message immediately when we receive a new message for the current chat
- Will send a read message when we read an unread message
- Will receive a read message when somebody reads our message
- Updates the unread messages counter badge on the application icon
- Syncs all messages that are received or changed back to Core Data
In this demo application, there will be no real API, WebSocket, or Core Data implementation to keep the Model implementation a bit more simple. Instead, I’ve added a chatbot that will start replying to you once you start a conversation. However, all other routings and calls are implemented like they would be if the storage and connections would be real, including small asynchronous pauses before returning.
The following three screens have been built:
Classic MVC
First of all, there’s the standard MVC pattern for building an iOS application. This is the way Apple structures all of its documentation code and the way APIs and UI elements expect to work. It’s what most people get taught when they take an iOS course.
Often MVC is blamed for leading to bloated UIViewController
s of a few thousand lines of code. But if it’s applied well, with a good separation between each layer, we can have quite slim ViewController
s that act only like intermediate managers between the View
s, Model
s, and other Controller
s.
Here’s the flowchart for the MVC implementation of the app (leaving out the CreateViewController
for clarity):
Let’s go over the layers in detail.
Model
The model layer is usually the least problematic layer in MVC. In this case, I opted to use ChatWebSocket
, ChatModel
, and PushNotificationController
to mediate between the Chat
and Message
objects, the external data sources and the rest of the application. ChatModel
is the source of truth within the application and only works in-memory in this demo application. In a real-life application, it would probably be backed by Core Data. Lastly, ChatEndpoint
handles all HTTP calls.
View
The views are quite large as it has to handle a lot of responsibilities since I’ve carefully separated all view code from the UIViewController
s. I’ve done the following:
- Used the (very recommendable) state
enum
pattern to define what the state the view is currently in. - Added the functions that get hooked up to the buttons and other action-triggering interface items (like tapping Return while entering a contact name.)
- Set up the constraints and call back to the delegate every time.
Once you throw a UITableView
in the mix, the views are now much larger than the UIViewController
s, leading to a worrisome 300+ lines of code and a lot of mixed tasks in the ChatView
.
Controller
As all of the model-handling logic has moved to ChatModel
. All of the view code—which might lurk in here in less optimal, separated projects—now lives in the view, so the UIViewController
s are pretty slim. The view controller is completely oblivious to what the model data looks like, how it is fetched, or how it should be displayed—it just coordinates. In the example project, none of the UIViewController
s goes over 150 lines of code.
However, the ViewController still does the following things:
- Being a delegate for the view and other view controllers
- Instantiating and pushing (or popping) view controllers if required
- Sending and receiving calls to and from the
ChatModel
- Starting and stopping the WebSocket depending on the stage of the view controller cycle
- Making logical decisions like not sending a message if it’s empty
- Updating the view
This is still a lot, but it’s mostly coordinating, processing callback blocks, and forwarding.
Benefits
- This pattern is understood by everyone and promoted by Apple
- Works with all documentation
- No extra frameworks needed
Downsides
- View controllers have a lot of tasks; a lot of them are basically passing data back and forth between the view and the model layer
- Not very fit to handle multiple event sources
- Classes tend to know a lot about other classes
Problem Definition
This works very well as long as the application follows the user’s actions and responds to them, as you would imagine an application like Adobe Photoshop or Microsoft Word would work. The user takes an action, the UI updates, repeat.
But modern applications are connected, often in more than one way. For example, you interact through a REST API, receive push notifications, and in some cases, you connect to a WebSocket as well.
With that, suddenly the view controller needs to deal with more sources of information, and whenever an external message is received without the user triggering it—like receiving a message through the WebSocket—the sources of information need to find their way back to the right view controllers. This needs a lot of code just to glue every part together to perform what is basically the same task.
External Data Sources
Let’s take a look at what happens when we get a push message:
class PushNotificationController {
class func received(notification: UNNotification,
whenProcessed result: (_ shouldShow: Bool) -> Void) {
let shouldShowNotification: Bool
defer {
result(shouldShowNotification)
}
let content = notification.request.content
let date = DateParser.date(from: content.subtitle) ?? Date()
let sender: Message.Sender = .other(name: content.title)
let pushedMessage = Message(with: sender, message: content.body, state: .sent, sendDate: date)
ChatModel.received(message: pushedMessage, by: content.title)
if let chatViewController = chatViewController(handling: content.title) {
chatViewController.received(message: pushedMessage)
shouldShowNotification = false
} else {
shouldShowNotification = true
}
updateChats(for: content.title)
}
private static func updateChats(for contact: String) {
guard let chat = ChatModel.loadedChats.first(where: { (chat) -> Bool in
chat.contact == contact
}) else {
return assertionFailure("Chat for received message should always exist")
}
BaseNavigationViewController.navigationController?.viewControllers.forEach({ (viewController) in
switch viewController {
case let chatsViewController as UpdatedChatDelegate:
chatsViewController.updated(chat: chat)
default:
break
}
})
}
private static func chatViewController(handling contact: String) -> ChatViewController? {
guard let lastViewController =
BaseNavigationViewController.navigationController?.viewControllers.last
as? ChatViewController,
lastViewController.chat.contact == contact else {
return nil
}
return lastViewController
}
}
We have to dig through the stack of view controllers manually to figure out if there’s a view controller that needs to update itself after we get a push notification. In this case, we also want to update the screens that implement the UpdatedChatDelegate
, which, in this case, is only the ChatsViewController
. We also do this to know if we should suppress the notification because we’re already looking at the Chat
it was meant for. In that case, we finally deliver the message to the view controller instead. It’s pretty clear that PushNotificationController
needs to know way too much about the application to be able to do its work.
If the ChatWebSocket
would be delivering messages to other parts of the application as well, instead of having a one-to-one relation to the ChatViewController
, we would face the same problem there.
It’s clear we have to write quite invasive code every time we add another external source. This code is also quite brittle, as it relies heavily on the application structure and delegates passing data back up into the hierarchy to work.
Delegates
The MVC pattern also adds extra complexity into the mix once we add other view controllers. This is because view controllers tend to know about each other through delegates, initializers, and—in the case of storyboards—prepareForSegue
when passing data and references. Every view controller handles its own connections to the model or mediating controllers, and they are both sending and receiving updates.
Also, the views communicate back to the view controllers through delegates. While this does work, it means there are quite a lot of steps we need to take to pass the data around, and I always find myself refactoring a lot around callbacks and checking if delegates are really set.
It’s possible to break one view controller by changing the code in another, like stale data in the ChatsListViewController
because the ChatViewController
is not calling updated(chat: Chat)
anymore. Especially in more complex scenarios, it’s a pain to keep everything in sync.
Separation between View and Model
By removing all view-related code from the view controller to customView
s and moving all of the model-related code to specialized controllers, the view controller is pretty lean and separated. However, there is still one problem left: There’s a gap between what the view wants to display and the data that resides in the model. A good example is the ChatListView
. What we want to display is a list of cells that tell us who we’re talking with, what the last message was, the last message date and how many unread messages are left in the Chat
:
However, we’re passing a model that doesn’t know about what we want to see. Instead, it’s just a Chat
with a contact, containing messages:
class Chat {
let contact: String
var messages: [Message]
init(with contact: String, messages: [Message] = []) {
self.contact = contact
self.messages = messages
}
}
Now it’s possible to quickly add some extra code that will get us the last message and the message count, but formatting dates to strings is a task that firmly belongs to the view layer:
var unreadMessages: Int {
return messages.filter {
switch ($0.sender, $0.state) {
case (.user, _), (.other, .read): return false
case (.other, .sending), (.other, .sent): return true
}
}.count
}
var lastMessage: Date? {
return messages.last?.sendDate
}
So finally we format the date in the ChatItemTableViewCell
when we display it:
func configure(with chat: Chat) {
participant.text = chat.contact
lastMessage.text = chat.messages.last?.message ?? ""
lastMessageDate.text = chat.lastMessage.map { lastMessageDate in
DateRenderer.string(from: lastMessageDate)
} ?? ""
show(unreadMessageCount: chat.unreadMessages)
}
Even in a fairly simple example, it’s pretty clear that there’s a tension between what the view needs and what the model provides.
Static Event-driven MVVM, a.k.a. a Static Event-driven Take on “the ViewModel Pattern”
Static MVVM works with view models, but instead of creating bidirectional traffic through them—much like we used to have through our view controller with MVC—we create immutable view models that update the UI every time the UI needs to change in response to an event.
An event can be triggered by almost any part of the code, as long as it’s able to provide the associated data that is required by the event enum
. For example, receiving the received(new: Message)
event can be triggered by a push notification, the WebSocket, or a regular network call.
Let’s see it in a diagram:
At first glance, it seems to be quite a bit more complex than the classic MVC example, as there are much more classes involved to accomplish exactly the same thing. But at closer inspection, none of the relations are bidirectional anymore.
Even more important is that every update to the UI gets triggered by an event, so there’s only one route through the app for everything that happens. It’s immediately clear what events you can expect. It’s also clear where you should add a new one if required, or add new behavior when responding to existing events.
After refactoring, I ended up with a lot of new classes, as I showed above. You can find my implementation of the static MVVM version on GitHub. However, when I compare the changes with the cloc
tool, it becomes clear that there’s actually not that much extra code at all:
Pattern | Files | Blank | Comment | Code |
---|---|---|---|---|
MVC | 30 | 386 | 217 | 1807 |
MVVM | 51 | 442 | 359 | 1981 |
There’s only a 9 percent increase in lines of code. More importantly, the average size of these files dropped from 60 lines of code to only 39.
Also crucially, the biggest drops can be found in the files that are typically the largest in MVC: the views and view controllers. The views are just 74 percent of their original sizes and the view controllers are now only 53 percent of their original size.
It should be noted too that a lot of the extra code is library code that helps to attach blocks to buttons and other objects in the visual tree, without requiring MVC’s classic @IBAction
or delegate patterns.
Let’s explore the different layers of this design one by one.
Event
The event is always an enum
, usually with associated values. Often they’ll overlap with one of the entities in your model but not necessarily so. In this case, the application is split into two main event enum
s: ChatEvent
and MessageEvent
. ChatEvent
is for all updates on the chat objects themselves:
enum ChatEvent {
case started
case loaded(chats: [Chat])
case creating(chat: Chat)
case created(chat: Chat)
case createChatFailed(reason: String)
}
The other one deals with all Message related events:
enum MessageEvent {
case sending(message: Message, contact: String, previousMessages: [Message])
case sent(message: Message, contact: String)
case failedSending(message: Message, contact: String, reason: String)
case received(message: Message, contact: String)
case userReads(messagesSentBy: String)
case userRead(othersMessages: [Message], sentBy: String)
case otherRead(yourMessage: Message, reader: String)
}
It’s important to limit your *Event
enum
s to a reasonable size. If you need 10 or more cases, that’s usually a sign you’re trying to cover more than one subject.
Note: The enum
concept is extremely powerful in Swift. I tend to use enum
s with associated values a lot, as they can take away a lot of ambiguity you would otherwise have with optional values.
Swift MVVM Tutorial: Event Router
The event router is the entry point for every event that happens in the application. Any class that can provide the associated value can create an event and send it to the event router. So they can be triggered by any kind of source, e.g.:
- The user segueing into a particular view controller
- The user tapping a certain button
- The application starting
- External events like:
- A network request returning with a failure or new data
- Push notifications
- WebSocket messages
The event router should know as little as possible about the source of the event and preferably nothing at all. None of the events in this example application have any indicator where they come from, so it’s very easy to mix in any kind of message source. For example, the WebSocket triggers the same event—received(message: Message, contact: String)
—as a new push notification.
Events are (you guessed it already) routed to the classes that need to further process these events. Usually, the only classes that are called are the model layer (if data needs to be added, changed, or removed) and the event handler. I’ll discuss both a bit more ahead, but the main feature of the event router is giving one easy access point to all events and forwarding the work to other classes. Here’s the ChatEventRouter
as an example:
class ChatEventRouter {
static func route(event: ChatEvent) {
switch event {
case .loaded(let chats):
ChatEventHandler.loaded(chats: chats)
case .creatingChat(let contact):
let chat = ChatModel.create(chatWith: contact)
ChatEndpoint.create(chat: chat)
ChatEventHandler.creatingChat()
case .created(let chat):
ChatEventHandler.created(chat: chat)
case .createChatFailed(let reason):
ChatEventHandler.failedCreatingChat(reason: reason)
}
}
}
There’s fairly little going on here: The only thing we’re doing is updating the model and forwarding the event to the ChatEventHandler
so the UI gets updated.
Swift MVVM Tutorial: Model Controller
This is exactly the same class as we use in MVC, as it was working pretty well already. It represents the state of the application and would usually be backed by Core Data or a local storage library.
Model layers—if implemented correctly in MVC—very rarely need any refactoring to fit different patterns. The largest change is that changing the model happens from fewer classes, making it a bit more clear where changes happen.
In an alternative take on this pattern, you could observe changes to the model and make sure they get handled. In this case, I chose to simply let only the *EventRouter
and *Endpoint
classes change the model, so there’s a clear responsibility of where and when the model gets updated. In contrast, if we were observing changes, we would have to write additional code to propagate non-model-changing events like errors through the ChatEventHandler
, which would make it less obvious how the events flow through the application.
Swift MVVM Tutorial: Event Handler
The event handler is the place where the views or view controllers can register (and deregister) themselves as listeners to receive updated view models, which are built whenever the ChatEventRouter
calls a function on the ChatEventHandler
.
You can see that it roughly reflects all of the view states we used in MVC before. If you want other types of UI updates—like sound or triggering the Taptic engine—they can be done from here as well.
protocol ChatListListening: class {
func updated(list: ChatListViewModel)
}
protocol CreateChatListening: class {
func updated(create: CreateChatViewModel)
}
class ChatEventHandler {
private static var chatListListening: [ChatListListening?] = []
private static var createChatListening: [CreateChatListening?] = []
class func add(listener: ChatListListening) {
weak var weakListener = listener
chatListListening.append(weakListener)
}
class func remove(listener: ChatListListening) {
chatListListening = chatListListening.filter { $0 !== listener }
}
class func add(listener: CreateChatListening) {
weak var weakListener = listener
createChatListening.append(weakListener)
listener.updated(create: CreateChatViewModelBuilder.build(isSending: false, error: nil))
}
class func remove(listener: CreateChatListening) {
createChatListening = createChatListening.filter { $0 !== listener }
}
class func started() {
ChatEndpoint.fetchChats()
let loadingViewModel = ChatListViewModelBuilder.buildLoading()
chatListListening.forEach { $0?.updated(list: loadingViewModel) }
}
class func loaded(chats: [Chat]) {
let chatList = ChatListViewModelBuilder.build(for: chats)
chatListListening.forEach { $0?.updated(list: chatList) }
}
class func creatingChat() {
let createChat = CreateChatViewModelBuilder.build(isSending: true, error: nil)
createChatListening.forEach { $0?.updated(create: createChat) }
}
class func failedCreatingChat(reason: String) {
let createChat = CreateChatViewModelBuilder.build(isSending: false, error: reason)
createChatListening.forEach { $0?.updated(create: createChat) }
}
class func created(chat: Chat) {
let createChat = CreateChatViewModelBuilder.build(isSending: false, error: nil)
createChatListening.forEach { $0?.updated(create: createChat) }
updateAllChatLists()
let chatViewController = ChatViewController(for: chat)
BaseNavigationViewController.pushViewController(chatViewController,
animated: true,
removePreviousFromStack: true)
}
class func updateAllChatLists() {
let chatListViewModel = ChatListViewModelBuilder.build(for: ChatModel.allChats())
chatListListening.forEach { $0?.updated(list: chatListViewModel) }
}
}
This class does nothing more than make sure that the right listener can get the right view model whenever a certain event has happened. New listeners can get a view model immediately when they’re added if that’s needed to set up their initial state. Always ensure you add a weak
reference to the list to prevent retention cycles.
Swift MVVM Tutorial: View Model
Here is one of the biggest differences between what a lot of MVVM patterns do versus what the static variant does. In this case, the view model is immutable instead of setting itself up as a permanent two-way-bound intermediate between model and view. Why would we do that? Let’s pause to explain it a moment.
One of the most important aspects of creating an application that works well in all possible cases is making sure that the state of the application is correct. If the UI doesn’t match the model or has outdated data, everything we do might lead to erroneous data being saved or the application crashing or behaving in an unexpected way.
One of the goals of applying this pattern is that we have no state in the application unless it’s absolutely necessary. What is state, exactly? State is basically every place where we store a representation of a particular type of data. One special type of state is the state your UI is currently in, which of course we cannot prevent with a UI-driven application. The other types of state are all data-related. If we have a copy of an array of Chat
s backing up our UITableView
in the Chat List screen, that’s an example of duplicate state. A traditional two-way-bound view model would be another example of a duplicate of our user’s Chat
s.
By passing an immutable view model that gets refreshed at every model change, we eliminate this type of duplicate state, because after it applies itself to the UI, it’s no longer used. Then we only have the only two types of state we cannot avoid—UI and model—and they are perfectly in sync with each other.
So the view model here is quite different from some MVVM applications. It only serves as an immutable data store for all flags, values, blocks and other values the view requires to reflect the state of the model, but it cannot be updated in any way by the View.
Therefore it can be a simple immutable struct
. To keep this struct
as simple as possible, we will instantiate it with a view model builder. One of the interesting things about a view model is that it gets behavioral flags like shouldShowBusy
and shouldShowError
that replace the state enum
mechanism previously found in the view. Here’s the data for the ChatItemTableViewCell
we had analyzed before:
struct ChatListItemViewModel {
let contact: String
let message: String
let lastMessageDate: String
let unreadMessageCount: Int
let itemTapped: () -> Void
}
Because the view model builder already takes care of the exact values and actions the view needs, all data is preformatted. Also new is a block that will be triggered once an item is tapped. Let’s see how it gets made by the view model builder.
View Model Builder
The view model builder can build instances of view models, transforming input like Chat
s or Message
s into view models that are perfectly tailored for a certain view. One of the most important things that happens in the view model builder is determining what actually happens inside the blocks in the view model. Blocks attached by the view model builder should be extremely short, calling functions of other parts of the architecture as soon as possible. Such blocks should not have any business logic.
class ChatListItemViewModelBuilder {
class func build(for chat: Chat) -> ChatListItemViewModel {
let lastMessageText = chat.messages.last?.message ?? ""
let lastMessageDate = (chat.messages.last?.sendDate).map { DateRenderer.string(from: $0) } ?? ""
let unreadMessageCount = ChatModel.unreadMessages(for: chat.contact).count
return ChatListItemViewModel(contact: chat.contact,
message: lastMessageText,
lastMessageDate: lastMessageDate,
unreadMessageCount: unreadMessageCount,
itemTapped: { show(chat: chat) })
}
private class func show(chat: Chat) {
let chatViewController = ChatViewController(for: chat)
BaseNavigationViewController.pushViewController(chatViewController, animated: true)
}
}
Now all preformatting happens in the same place and the behavior gets decided here as well. It’s quite an important class in this hierarchy and it can be interesting to see how the different builders in the demo application have been implemented and deal with more complicated scenarios.
Swift MVVM Tutorial: View Controller
The view controller in this architecture does very little. It will set up and tear down everything related to its view. It’s best fit to do this because it gets all lifecycle callbacks that are required to add and remove listeners at the right time.
Sometimes it needs to update a UI element that is not covered by the root view, like the title or a button in the navigation bar. That’s why I usually still register the view controller as a listener to the event router if I have a view model that covers the whole view for the given view controller; I forward the view model to the view afterward. But it’s also fine to register any UIView
as the listener directly if there is a part of the screen that has a different update rate, e.g. a live stock ticker on top of a page about a certain company.
The code for the ChatsViewController
is now so short that it takes less than a page. What’s left is overriding the base view, adding and removing the add button from the navigation bar, setting the title, adding itself as a listener, and implementing the ChatListListening
protocol:
class ChatsViewController: UIViewController {
private lazy var customView: ChatsView = {
let customView = ChatsView()
return customView
}()
private var addButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add,
target: nil, action: nil)
override func loadView() {
view = customView
}
override func viewDidLoad() {
super.viewDidLoad()
ChatEventHandler.add(listener: self)
ChatEventRouter.route(event: .started)
title = "Chats"
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationItem.rightBarButtonItem = addButton
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationItem.rightBarButtonItem = nil
}
}
extension ChatsViewController: ChatListListening {
func updated(list: ChatListViewModel) {
addButton.action(block: { _ in
list.addChat()
})
customView.display(viewModel: list)
}
}
There isn’t anything left that can be done elsewhere, as the ChatsViewController
is stripped to its bare minimum.
Swift MVVM Tutorial: View
The view in the immutable MVVM architecture can still be quite heavy, as it still has a list of tasks, but I managed to strip it of the following responsibilities compared to the MVC architecture:
- Determining what needs to change in response to a new state
- Implementing delegates and functions for actions
- Handle view-to-view triggers like gestures and triggered animations
- Transforming data in such a way that it can be shown (like
Date
s toString
s)
Especially the last point has quite a large advantage. In MVC, when the view or view controller is responsible for transforming the data for display, it will always do this on the main thread as it’s very hard to separate true changes to the UI that are required to happen on this thread from things that are not required to run on it. And having non-UI-change code running on the main thread can lead to a less responsive application.
Instead, with this MVVM pattern, everything from the block that’s triggered by a tap until the moment the view model is built and will be passed to the listener—we can run this all on a separate thread and only dip into the main thread in the end for doing UI updates. If our application spends less time on the main thread, it will run smoother.
Once the view model applies the new state to the view, it’s allowed to evaporate instead of lingering around as another layer of state. Everything that might trigger an event is attached to an item in the view and we won’t communicate back to the view model.
One thing is important to remember: You are not forced to map a view model through a view controller to a view. As mentioned before, parts of the view can be managed by other view models, especially when update rates vary. Consider a Google Sheet being edited by different people while keeping a chat pane open for collaborators—it’s not very useful to refresh the document whenever a chat message arrives.
A well-known example is a type-to-find implementation where the search box gets updated with more accurate results as we enter more text. This is how I would implement autocomplete in the CreateAutocompleteView
class: The whole screen is served by the CreateViewModel
but the text box is listening to the AutocompleteContactViewModel
instead.
Another example is using a form validator, which can either be built as a “local loop” (attaching or removing error states to fields and declaring a form to be valid) or done through triggering an event.
Static Immutable View Models Provide Better Separation
By using a static MVVM implementation we’ve managed to finally separate all of the layers completely because the view model now bridges between the model and the view. We also made it easier to manage events that were not caused by user action and removed a lot of the dependencies between the different parts of our application. The only thing a view controller does is to register (and deregister) itself to the event handlers as a listener for the events it wants to receive.
Benefits:
- View and view controller implementations tend to be much lighter
- Classes are more specialized and separated
- Events can be triggered easily from any place
- Events follow a predictable path through the system
- State is only updated from one place
- App can be more performant as it’s easier to do work off the main thread
- Views receive tailor-made view models and are perfectly separated from the models
Downsides:
- A full view model is created and sent every time the UI needs to update, often overwriting the same button text with the same button text, and replacing blocks with blocks that do exactly the same
- Requires some helper extensions to make button taps and other UI events work well with the blocks in the view model
- Event
enum
s can easily grow pretty large in complex scenarios and might be hard to split up
The great thing is that this is a pure Swift pattern: It does not require a third-party Swift MVVM framework, nor does it exclude the use of classic MVC, so you can easily add new features or refactor problematic parts of your application today without being forced to rewrite your whole application.
There are other approaches to combat large view controllers that provide better separation as well. I couldn’t include them all in full detail to compare them, but let’s take a brief look at some of the alternatives:
- Some form of the MVVM pattern
- Some form of Reactive (using RxSwift, sometimes combined with MVVM)
- The model-view-presenter pattern (MVP)
- The view-interactor-presenter-entity-router pattern (VIPER)
Traditional MVVM replaces most of the view controller code with a view model that is just a regular class and can be tested more easily in isolation. Since it needs to be a bi-directional bridge between the view and the model it often implements some form of Observables. That’s why you often see it used together with a framework like RxSwift.
MVP and VIPER deal with extra abstraction layers between the model and the view in a more traditional way, while Reactive really remodels the way data and events flow through your application.
The Reactive style of programming is gaining a lot of popularity lately and actually is pretty close to the static MVVM approach with events, as explained in this article. The major difference is that it usually requires a framework, and a lot of your code is specifically geared towards that framework.
MVP is a pattern where both the view controller and the view are considered to be the view layer. The presenter transforms the model and passes it to the view layer, while I transform the data into a view model first. Since the view can be abstracted to a protocol, it’s much easier to test.
VIPER takes the presenter from MVP, adds a separate “interactor” for business logic, calls the model layer “entity,” and has a router for navigation purposes (and to complete the acronym). It can be considered a more detailed and decoupled form of MVP.
So there you have it: static event-driven MVVM explained. I look forward to hearing from you in the comments below!
Understanding the basics
What is the use of MVVM?
A view model is a separate and easy-to-test class that takes over all of the logic and model-to-view code—and often also the view-to-model binding—from the view controller.
What are protocols in iOS?
Protocols (often named “interfaces” in other languages) are a set of functions and variables that can be implemented by any class or struct. Since protocols are not bound to a specific class, it’s possible to use any class for a protocol reference as long as it implements it. This makes it much more flexible.
What is the delegation pattern in iOS?
A delegate is a weak reference to another class based on a protocol. Delegates are usually used to “report back” to another object after completing a task, without tying yourself to a specific class or knowing about all of its details.
What is the difference between MVC and MVVM?
In iOS, MVVM isn’t a replacement for MVC, it’s an addition. The view controller still plays a role, but the view model becomes the intermediate between the view and the model.
What is MVP in iOS?
In iOS, the MVP (model-view-presenter) pattern is one where UIViews and UIViewControllers are both part of the view layer. (Confusingly, the view layer is an architectural concept, while a UIView is from UIKit and is also usually called a view.)
Rotterdam, Netherlands
Member since December 8, 2015
About the author
A mobile developer and teacher, Lucas has built not only iOS apps but also back-end APIs using Swift, Objective-C, Python, Elixir, and C#.