Introduction to the MVVM Design Pattern in Swift Tutorial

When starting a new iOS project, after receiving design documents and forming a development plan, you begin by implementing the UI. This involves translating design sketches into code, using elements like UITextField, UITableView, UILabel, UIButton, IBOutlets, and IBActions.

However, challenges arise when you need to add functionality to these UI elements. Handling button touches, populating labels and table views with data, and managing UI states can lead to a significant increase in code size and complexity. Suddenly, your once-manageable codebase balloons to over 3,000 lines.

3,000 lines of Swift code

This often results in what’s known as spaghetti code, making your project difficult to maintain and scale.

One approach to address this is by adopting the Model-View-Controller (MVC) design pattern. While MVC can introduce some structure, it also has its limitations. This is where the Model-View-ViewModel (MVVM) pattern proves to be a more effective solution.

Tackling Spaghetti Code

As your project grows, your initial ViewController often becomes overloaded with responsibilities. It ends up handling everything from network requests and data parsing to UI presentation logic and state management. This makes the code tightly coupled, difficult to reuse, and prone to errors.

This scenario typically arises when developers prioritize speed over maintainability. Temporary workarounds, like adding networking code directly into the ViewController to quickly test data display, gradually accumulate and contribute to the problem.

The UIViewController, being the foundation of UI code in iOS, often becomes a catch-all for various functionalities. While Apple’s UI abstraction relies on the UIViewController as part of the MVC pattern, it doesn’t mean that all code related to both the view and the controller should reside within it.

Transitioning to MVC

MVC Design Pattern

In the MVC pattern, the View should ideally remain passive, solely responsible for displaying data provided to it. The Controller acts as an intermediary, fetching data from the Model, processing it, and then supplying it to the View for presentation. The View, in turn, notifies the Controller of any user interactions.

Given that the UIViewController inherently combines aspects of both view and controller, it’s tempting to place code related to subviews directly within it. However, a better practice is to encapsulate such code within custom UIView subclasses.

Failure to maintain this separation can lead to tangled code where the responsibilities of the View and Controller become intertwined.

MVVM to the Rescue

The MVVM pattern offers a cleaner solution by separating concerns more effectively. It leverages the existing structure of UIViewController as a View, combining it with the Controller aspects of MVC into a single entity.

MVVM Design Pattern

In MVVM:

  • Model remains the same as in MVC, representing raw data.
  • View, comprising UIView or UIViewController along with their associated .xib and .storyboard files, focuses solely on displaying prepared data. It shouldn’t contain logic for data formatting or processing.
  • ViewModel acts as an intermediary between the Model and View. It handles asynchronous operations, data transformation for UI presentation, and responses to Model changes. It exposes a well-defined API tailored to the specific View it supports.

One of the key advantages of MVVM is improved testability. Since the ViewModel is a pure NSObject (or struct), it remains independent of UI code, making it easier to unit test without the need for complex UI setups.

With MVVM, the View becomes significantly simpler, while the ViewModel takes on the responsibility of bridging the gap between the Model and View.

Implementing MVVM in Swift

MVVM In Swift

To illustrate MVVM in action, you can refer to the example Xcode project provided for this Swift MVVM tutorial here. This project utilizes Swift 3 and Xcode 8.1.

Two versions of the project are available: Starter and Finished.

The Finished version showcases a complete mini-application built with Swift MVVM architecture, while the Starter version provides the same project without the implemented methods and objects.

Begin by downloading the Starter project and following along with this tutorial. You can refer to the Finished project for a quick reference later on.

Tutorial Project Overview

The tutorial focuses on a basketball application designed to track player actions during a game.

Basketball application

This app allows for real-time tracking of player moves and the overall score in a pickup game. Two teams compete until a score of 15 is reached (with a minimum two-point difference). Players can score one or two points, and their actions, such as assists, rebounds, and fouls, can be recorded.

The project structure is as follows:

Project hierarchy

Model

  • Game.swift: Contains the game logic, tracks the overall score, and monitors each player’s moves.
  • Team.swift: Stores the team name and a list of players (three players per team).
  • Player.swift: Represents a single player with a name.

View

  • HomeViewController.swift: Acts as the root view controller, presenting the GameScoreboardEditorViewController.
  • GameScoreboardEditorViewController.swift: Complemented by an Interface Builder view in Main.storyboard. This screen is the primary focus of the tutorial.
  • PlayerScoreboardMoveEditorView.swift: Supplemented with an Interface Builder view in PlayerScoreboardMoveEditorView.xib. This is a subview of the GameScoreboardEditorViewController and also utilizes the MVVM pattern.

ViewModel

  • The ViewModel group is currently empty; this is where you will be building your ViewModels.

The provided Xcode project includes placeholders for View objects (UIView and UIViewController) and custom objects (Services group) that demonstrate data provision to ViewModel objects.

The Extensions group contains helpful UI code extensions, which are self-explanatory and not within the scope of this tutorial.

Running the app at this stage will display the completed UI, but button presses won’t have any effect yet. This is because you’ve only set up the views and IBActions without connecting them to the app’s logic or populating the UI elements with data from the Model (the Game object).

Linking View and Model with ViewModel

In MVVM, the View should remain unaware of the Model’s existence. Its interaction is limited to a ViewModel.

Start by examining your View. In GameScoreboardEditorViewController.swift, the fillUI method is currently empty. This is where you’ll populate the UI with data. To achieve this, you need to supply data to the ViewController using a ViewModel object.

Begin by creating a ViewModel object that holds all the necessary data for this ViewController.

Navigate to the ViewModel Xcode project group (which will be empty), create a new file named GameScoreboardEditorViewModel.swift, and define a protocol within it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import Foundation

protocol GameScoreboardEditorViewModel {
    var homeTeam: String { get }
    var awayTeam: String { get }
    var time: String { get }
    var score: String { get }
    var isFinished: Bool { get }
    
    var isPaused: Bool { get }
    func togglePause();
}

Using protocols in this manner helps maintain a clean and organized structure. You define only the data that will be utilized.

Next, create a concrete implementation for this protocol. Create a new file named GameScoreboardEditorViewModelFromGame.swift and make it a subclass of NSObject.

Also, make it conform to the GameScoreboardEditorViewModel protocol:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import Foundation

class GameScoreboardEditorViewModelFromGame: NSObject, GameScoreboardEditorViewModel {
    
    let game: Game
    
    struct Formatter {
        static let durationFormatter: DateComponentsFormatter = {
            let dateFormatter = DateComponentsFormatter()
            dateFormatter.unitsStyle = .positional
            return dateFormatter
        }()
    }
    
    // MARK: GameScoreboardEditorViewModel protocol
    
    var homeTeam: String
    var awayTeam: String
    
    var time: String
    var score: String
    var isFinished: Bool
    
    var isPaused: Bool
    func togglePause() {
        if isPaused {
            startTimer()
        } else {
            pauseTimer()
        }
        
        self.isPaused = !isPaused
    }
    
    // MARK: Init
    
    init(withGame game: Game) {
        self.game = game
        
        self.homeTeam = game.homeTeam.name
        self.awayTeam = game.awayTeam.name
        
        self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game)
        self.score = GameScoreboardEditorViewModelFromGame.scorePretty(for: game)
        self.isFinished = game.isFinished
        self.isPaused = true
    }
    
    // MARK: Private
    
    fileprivate var gameTimer: Timer?
    fileprivate func startTimer() {
        let interval: TimeInterval = 0.001
        gameTimer = Timer.schedule(repeatInterval: interval) { timer in
            self.game.time += interval
            self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game)
        }
    }
    
    fileprivate func pauseTimer() {
        gameTimer?.invalidate()
        gameTimer = nil
    }
    
    // MARK: String Utils
    
    fileprivate static func timeFormatted(totalMillis: Int) -> String {
        let millis: Int = totalMillis % 1000 / 100 // "/ 100" <- because we want only 1 digit
        let totalSeconds: Int = totalMillis / 1000
        
        let seconds: Int = totalSeconds % 60
        let minutes: Int = (totalSeconds / 60)
        
        return String(format: "%02d:%02d.%d", minutes, seconds, millis)
    }
    
    fileprivate static func timeRemainingPretty(for game: Game) -> String {
        return timeFormatted(totalMillis: Int(game.time * 1000))
    }
    
    fileprivate static func scorePretty(for game: Game) -> String {
        return String(format: "\(game.homeTeamScore) - \(game.awayTeamScore)")
    }
    
}

Notice that you provide everything the ViewModel needs to function through its initializer. This includes the Game object, representing the Model underlying this ViewModel.

If you run the app now, it still won’t function correctly because you haven’t connected this ViewModel data to the View itself.

To address this, go back to GameScoreboardEditorViewController.swift and create a public property named viewModel. Make it of type GameScoreboardEditorViewModel. Place this property declaration right before the viewDidLoad method inside GameScoreboardEditorViewController.swift.

1
2
3
4
5
var viewModel: GameScoreboardEditorViewModel? {
    didSet {
        fillUI()
    }
}

Next, implement the fillUI method.

Observe that this method is called from two locations: the viewModel property observer (didSet) and the viewDidLoad method. This is because a ViewController can be created and assigned a ViewModel before its view is attached (before viewDidLoad is called).

Conversely, you could attach the ViewController’s view to another view, triggering viewDidLoad. However, if viewModel isn’t set at that point, nothing will happen.

Therefore, it’s crucial to verify that all necessary data is available before attempting to populate the UI. Guarding against unexpected usage is essential.

In the fillUI method, replace its contents with the following code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
fileprivate func fillUI() {
    if !isViewLoaded {
        return
    }
    
    guard let viewModel = viewModel else {
        return
    }
    
    // we are sure here that we have all the setup done
    
    self.homeTeamNameLabel.text = viewModel.homeTeam
    self.awayTeamNameLabel.text = viewModel.awayTeam
    
    self.scoreLabel.text = viewModel.score
    self.timeLabel.text = viewModel.time
    
    let title: String = viewModel.isPaused ? "Start" : "Pause"
    self.pauseButton.setTitle(title, for: .normal)
}

Now, implement the pauseButtonPress method:

1
2
3
@IBAction func pauseButtonPress(_ sender: AnyObject) {
    viewModel?.togglePause()
}

The remaining step is to set the actual viewModel property on this ViewController. You’ll do this “from the outside.”

Open HomeViewController.swift and uncomment the ViewModel creation and setup lines within the showGameScoreboardEditorViewController method:

1
2
3
// uncomment this when view model is implemented
let viewModel = GameScoreboardEditorViewModelFromGame(withGame: game)
controller.viewModel = viewModel

Run the app now. It should resemble the following:

iOS App

The middle view, displaying the score, time, and team names, no longer shows the placeholder values set in Interface Builder. It now presents values retrieved from the ViewModel object, which in turn fetches its data from the actual Model (Game object).

Great progress! However, the player views and their buttons remain non-functional.

You have six views dedicated to tracking player moves. These are implemented using a separate subview called PlayerScoreboardMoveEditorView, which currently doesn’t interact with real data and only displays static values set in Interface Builder within the PlayerScoreboardMoveEditorView.xib file.

You need to provide these views with dynamic data, following the same approach used for GameScoreboardEditorViewController and GameScoreboardEditorViewModel.

Open the ViewModel group in your Xcode project and define a new protocol for this purpose. Create a new file named PlayerScoreboardMoveEditorViewModel.swift and add the following code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import Foundation

protocol PlayerScoreboardMoveEditorViewModel {
    var playerName: String { get }
    
    var onePointMoveCount: String { get }
    var twoPointMoveCount: String { get }
    var assistMoveCount: String { get }
    var reboundMoveCount: String { get }
    var foulMoveCount: String { get }
    
    func onePointMove()
    func twoPointsMove()
    func assistMove()
    func reboundMove()
    func foulMove()
}

This ViewModel protocol is designed to match your PlayerScoreboardMoveEditorView, mirroring the structure you established for the parent view (GameScoreboardEditorViewController). It includes properties for the five move types, a mechanism to handle user touches on action buttons, and a String property for the player’s name.

Next, create a concrete class that implements this protocol, similar to what you did for the parent view (GameScoreboardEditorViewController). Create a new file named PlayerScoreboardMoveEditorViewModelFromPlayer.swift and make it a subclass of NSObject. Make it conform to the PlayerScoreboardMoveEditorViewModel protocol:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import Foundation

class PlayerScoreboardMoveEditorViewModelFromPlayer: NSObject, PlayerScoreboardMoveEditorViewModel {
    
    fileprivate let player: Player
    fileprivate let game: Game
    
    // MARK: PlayerScoreboardMoveEditorViewModel protocol
    
    let playerName: String
    
    var onePointMoveCount: String
    var twoPointMoveCount: String
    var assistMoveCount: String
    var reboundMoveCount: String
    var foulMoveCount: String
    
    func onePointMove() {
        makeMove(.onePoint)
    }
    
    func twoPointsMove() {
        makeMove(.twoPoints)
    }
    
    func assistMove() {
        makeMove(.assist)
    }
    
    func reboundMove() {
        makeMove(.rebound)
    }
    
    func foulMove() {
        makeMove(.foul)
    }
    
    // MARK: Init
    
    init(withGame game: Game, player: Player) {
        self.game = game
        self.player = player
        
        self.playerName = player.name
        self.onePointMoveCount = "\(game.playerMoveCount(for: player, move: .onePoint))"
        self.twoPointMoveCount = "\(game.playerMoveCount(for: player, move: .twoPoints))"
        self.assistMoveCount = "\(game.playerMoveCount(for: player, move: .assist))"
        self.reboundMoveCount = "\(game.playerMoveCount(for: player, move: .rebound))"
        self.foulMoveCount = "\(game.playerMoveCount(for: player, move: .foul))"
    }
    
    // MARK: Private
    
    fileprivate func makeMove(_ move: PlayerInGameMove) {
        game.addPlayerMove(move, for: player)
        
        onePointMoveCount = "\(game.playerMoveCount(for: player, move: .onePoint))"
        twoPointMoveCount = "\(game.playerMoveCount(for: player, move: .twoPoints))"
        assistMoveCount = "\(game.playerMoveCount(for: player, move: .assist))"
        reboundMoveCount = "\(game.playerMoveCount(for: player, move: .rebound))"
        foulMoveCount = "\(game.playerMoveCount(for: player, move: .foul))"
    }
    
}

Now, you need an object responsible for creating instances of this ViewModel and setting them as properties within PlayerScoreboardMoveEditorView.

Recall that HomeViewController handled setting the viewModel property on GameScoreboardEditorViewController. Similarly, GameScoreboardEditorViewController acts as the parent view for PlayerScoreboardMoveEditorView and will be responsible for creating PlayerScoreboardMoveEditorViewModel objects.

Start by expanding your GameScoreboardEditorViewModel. Open GameScoreboardEditorViewModel and add two new properties:

1
2
var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get }
var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get }

Update GameScoreboardEditorViewModelFromGame by adding these two properties just above the initWithGame method:

1
2
let homePlayers: [PlayerScoreboardMoveEditorViewModel]
let awayPlayers: [PlayerScoreboardMoveEditorViewModel]

Add these two lines inside initWithGame:

1
2
self.homePlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.homeTeam.players, game: game)
self.awayPlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.awayTeam.players, game: game)

Implement the missing playerViewModelsWithPlayers method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// MARK: Private Init

fileprivate static func playerViewModels(from players: [Player], game: Game) -> [PlayerScoreboardMoveEditorViewModel] {
    var playerViewModels: [PlayerScoreboardMoveEditorViewModel] = [PlayerScoreboardMoveEditorViewModel]()
    for player in players {
        playerViewModels.append(PlayerScoreboardMoveEditorViewModelFromPlayer(withGame: game, player: player))
    }
    
    return playerViewModels
}

You’ve now updated your ViewModel (GameScoreboardEditorViewModel) to include arrays for both home and away players. The next step is to populate these arrays.

You’ll handle this in the same location where you used the viewModel to populate the main UI.

Open GameScoreboardEditorViewController and navigate to the fillUI method. Add these lines at the end of the method:

1
2
3
4
5
6
7
homePlayer1View.viewModel = viewModel.homePlayers[0]
homePlayer2View.viewModel = viewModel.homePlayers[1]
homePlayer3View.viewModel = viewModel.homePlayers[2]
        
awayPlayer1View.viewModel = viewModel.awayPlayers[0]
awayPlayer2View.viewModel = viewModel.awayPlayers[1]
awayPlayer3View.viewModel = viewModel.awayPlayers[2]

You’ll encounter build errors at this point because you haven’t added the actual viewModel property to PlayerScoreboardMoveEditorView.

Add the following code above the init method inside PlayerScoreboardMoveEditorView:

1
2
3
4
5
var viewModel: PlayerScoreboardMoveEditorViewModel? {
    didSet {
        fillUI()
    }
}

Implement the fillUI method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fileprivate func fillUI() {
    guard let viewModel = viewModel else {
        return
    }
    
    self.name.text = viewModel.playerName
    
    self.onePointCountLabel.text = viewModel.onePointMoveCount
    self.twoPointCountLabel.text = viewModel.twoPointMoveCount
    self.assistCountLabel.text = viewModel.assistMoveCount
    self.reboundCountLabel.text = viewModel.reboundMoveCount
    self.foulCountLabel.text = viewModel.foulMoveCount
}

Finally, run the app and observe how the UI elements now display data retrieved from the Game object.

iOS App

You have successfully built a functional MVVM Swift app! It effectively hides the Model from the View, resulting in a much simpler View compared to a traditional MVC implementation.

Currently, you have an app with a View and its corresponding ViewModel. This View also contains six instances of the same subview (player view), each with its ViewModel.

However, you might notice that you can only display data in the UI once (within the fillUI method), and this data remains static.

If your view’s data won’t change during its lifecycle, this approach to implementing MVVM is sufficient and provides a clean solution.

Making the ViewModel Dynamic

To accommodate data changes, you need to make your ViewModel dynamic. This means that whenever the Model changes, the ViewModel should update its public properties, propagating these changes back to the View, which will then refresh the UI.

Numerous approaches can achieve this.

When the Model changes, the ViewModel should be the first to be notified. You need a mechanism to communicate these changes to the View.

Options include using RxSwift, a comprehensive library that might take some time to master.

The ViewModel could emit NSNotifications upon each property value change. However, this introduces a significant amount of code for handling notification subscriptions and unsubscriptions, especially when the view is deallocated.

Key-Value-Observing (KVO) is another option, but its API might not be the most elegant.

In this tutorial, you’ll leverage Swift generics and closures, which are well-explained in the Bindings, Generics, Swift and MVVM article.

Let’s return to the MVVM Swift example app.

Go to the ViewModel project group and create a new Swift file named Dynamic.swift.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Dynamic<T> {
    typealias Listener = (T) -> ()
    var listener: Listener?
    
    func bind(_ listener: Listener?) {
        self.listener = listener
    }
    
    func bindAndFire(_ listener: Listener?) {
        self.listener = listener
        listener?(value)
    }
    
    var value: T {
        didSet {
            listener?(value)
        }
    }
    
    init(_ v: T) {
        value = v
    }
}

This class will be used for properties in your ViewModels that are expected to change during the View’s lifecycle.

Start with the PlayerScoreboardMoveEditorView and its ViewModel, PlayerScoreboardMoveEditorViewModel.

Open PlayerScoreboardMoveEditorViewModel and examine its properties. Since the playerName is not expected to change, you can leave it as is.

The other five properties (representing the move counts) will change, so you need to modify them. You’ll use the Dynamic class you just added to the project.

Inside PlayerScoreboardMoveEditorViewModel, remove the definitions for the five String properties representing move counts and replace them with:

1
2
3
4
5
var onePointMoveCount: Dynamic<String> { get }
var twoPointMoveCount: Dynamic<String> { get }
var assistMoveCount: Dynamic<String> { get }
var reboundMoveCount: Dynamic<String> { get }
var foulMoveCount: Dynamic<String> { get }

Your ViewModel protocol should now look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import Foundation

protocol PlayerScoreboardMoveEditorViewModel {
    var playerName: String { get }
    
    var onePointMoveCount: Dynamic<String> { get }
    var twoPointMoveCount: Dynamic<String> { get }
    var assistMoveCount: Dynamic<String> { get }
    var reboundMoveCount: Dynamic<String> { get }
    var foulMoveCount: Dynamic<String> { get }
    
    func onePointMove()
    func twoPointsMove()
    func assistMove()
    func reboundMove()
    func foulMove()
}

The Dynamic type allows you to change the value of a property while simultaneously notifying any registered listener objects about the change. In this case, the listener will be the View.

Now, update the actual ViewModel implementation, PlayerScoreboardMoveEditorViewModelFromPlayer.

Replace the following:

1
2
3
4
5
var onePointMoveCount: String
var twoPointMoveCount: String
var assistMoveCount: String
var reboundMoveCount: String
var foulMoveCount: String

with:

1
2
3
4
5
let onePointMoveCount: Dynamic<String>
let twoPointMoveCount: Dynamic<String>
let assistMoveCount: Dynamic<String>
let reboundMoveCount: Dynamic<String>
let foulMoveCount: Dynamic<String>

Note: Declaring these properties as constants with let is acceptable because you won’t modify the properties themselves. Instead, you’ll be updating the value property of the Dynamic object.

You’ll encounter build errors because you haven’t initialized the Dynamic objects yet.

Inside the init method of PlayerScoreboardMoveEditorViewModelFromPlayer, replace the initialization of move properties with:

1
2
3
4
5
self.onePointMoveCount = Dynamic("\(game.playerMoveCount(for: player, move: .onePoint))")
self.twoPointMoveCount = Dynamic("\(game.playerMoveCount(for: player, move: .twoPoints))")
self.assistMoveCount = Dynamic("\(game.playerMoveCount(for: player, move: .assist))")
self.reboundMoveCount = Dynamic("\(game.playerMoveCount(for: player, move: .rebound))")
self.foulMoveCount = Dynamic("\(game.playerMoveCount(for: player, move: .foul))")

In PlayerScoreboardMoveEditorViewModelFromPlayer, locate the makeMove method and replace it with:

1
2
3
4
5
6
7
8
9
fileprivate func makeMove(_ move: PlayerInGameMove) {
    game.addPlayerMove(move, for: player)
    
    onePointMoveCount.value = "\(game.playerMoveCount(for: player, move: .onePoint))"
    twoPointMoveCount.value = "\(game.playerMoveCount(for: player, move: .twoPoints))"
    assistMoveCount.value = "\(game.playerMoveCount(for: player, move: .assist))"
    reboundMoveCount.value = "\(game.playerMoveCount(for: player, move: .rebound))"
    foulMoveCount.value = "\(game.playerMoveCount(for: player, move: .foul))"
}

As you can see, you create instances of the Dynamic class and assign initial String values to them. When you need to update the data, you modify the value property of the Dynamic object instead of the property itself.

Great! PlayerScoreboardMoveEditorViewModel is now dynamic.

Let’s utilize this dynamic behavior by modifying the view that will listen for these changes.

Open PlayerScoreboardMoveEditorView and navigate to its fillUI method. You should see build errors in this method because you’re attempting to assign String values to properties of type Dynamic<String>.

Replace the “errored” lines:

1
2
3
4
5
self.onePointCountLabel.text = viewModel.onePointMoveCount
self.twoPointCountLabel.text = viewModel.twoPointMoveCount
self.assistCountLabel.text = viewModel.assistMoveCount
self.reboundCountLabel.text = viewModel.reboundMoveCount
self.foulCountLabel.text = viewModel.foulMoveCount

with:

1
2
3
4
5
viewModel.onePointMoveCount.bindAndFire { [unowned self] in self.onePointCountLabel.text = $0 }
viewModel.twoPointMoveCount.bindAndFire { [unowned self] in self.twoPointCountLabel.text = $0 }
viewModel.assistMoveCount.bindAndFire { [unowned self] in self.assistCountLabel.text = $0 }
viewModel.reboundMoveCount.bindAndFire { [unowned self] in self.reboundCountLabel.text = $0 }
viewModel.foulMoveCount.bindAndFire { [unowned self] in self.foulCountLabel.text = $0 }

Next, implement the five methods representing move actions (in the Button Action section):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@IBAction func onePointAction(_ sender: Any) {
    viewModel?.onePointMove()
}

@IBAction func twoPointsAction(_ sender: Any) {
    viewModel?.twoPointsMove()
}

@IBAction func assistAction(_ sender: Any) {
    viewModel?.assistMove()
}

@IBAction func reboundAction(_ sender: Any) {
    viewModel?.reboundMove()
}

@IBAction func foulAction(_ sender: Any) {
    viewModel?.foulMove()
}

Run the app and interact with the move buttons. Observe how the counter values within the player views update when you click the action buttons.

iOS App

You’ve successfully implemented dynamic updates for PlayerScoreboardMoveEditorView and its ViewModel, PlayerScoreboardMoveEditorViewModel.

Now, apply the same principles to your main view, GameScoreboardEditorViewController.

Open GameScoreboardEditorViewModel and identify which values are expected to change during the view’s lifecycle.

Replace the definitions of time, score, isFinished, and isPaused with their Dynamic counterparts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import Foundation

protocol GameScoreboardEditorViewModel {
    var homeTeam: String { get }
    var awayTeam: String { get }
    var time: Dynamic<String> { get }
    var score: Dynamic<String> { get }
    var isFinished: Dynamic<Bool> { get }
    
    var isPaused: Dynamic<Bool> { get }
    func togglePause()
    
    var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get }
    var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get }
}

Go to the ViewModel implementation (GameScoreboardEditorViewModelFromGame) and perform the same replacements for the properties declared in the protocol.

Replace:

1
2
3
4
5
var time: String
var score: String
var isFinished: Bool
 
var isPaused: Bool

with:

1
2
3
4
5
let time: Dynamic<String>
let score: Dynamic<String>
let isFinished: Dynamic<Bool>
    
let isPaused: Dynamic<Bool>

You’ll encounter build errors because the ViewModel’s property types have changed from String and Bool to Dynamic<String> and Dynamic<Bool>. Let’s address these errors.

Fix the togglePause method by replacing its contents with:

1
2
3
4
5
6
7
8
9
func togglePause() {
    if isPaused.value {
        startTimer()
    } else {
        pauseTimer()
    }
        
    self.isPaused.value = !isPaused.value
}

The key change here is that you’re no longer setting the property value directly on the property. Instead, you set it on the value property of the Dynamic object.

Next, fix the initWithGame method. Replace:

1
2
3
4
self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(game)
self.score = GameScoreboardEditorViewModelFromGame.scorePretty(game)
self.isFinished = game.isFinished
self.isPaused = true

with:

1
2
3
4
self.time = Dynamic(GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game))
self.score = Dynamic(GameScoreboardEditorViewModelFromGame.scorePretty(for: game))
self.isFinished = Dynamic(game.isFinished)
self.isPaused = Dynamic(true)

By now, you should understand the pattern. You’re wrapping primitive values like String, Int, and Bool with their Dynamic<T> counterparts to enable a lightweight binding mechanism.

You have one more error to resolve. In the startTimer method, replace the erroneous line with:

1
self.time.value = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game)

You’ve successfully upgraded your ViewModel to be dynamic, just like you did with the player’s ViewModel. However, you still need to update your View (GameScoreboardEditorViewController).

Replace the entire fillUI method with the following code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
fileprivate func fillUI() {
    if !isViewLoaded {
        return
    }
    
    guard let viewModel = viewModel else {
        return
    }
    
    self.homeTeamNameLabel.text = viewModel.homeTeam
    self.awayTeamNameLabel.text = viewModel.awayTeam
    
    viewModel.score.bindAndFire { [unowned self] in self.scoreLabel.text = $0 }
    viewModel.time.bindAndFire { [unowned self] in self.timeLabel.text = $0 }
    
    viewModel.isFinished.bindAndFire { [unowned self] in
        if $0 {
            self.homePlayer1View.isHidden = true
            self.homePlayer2View.isHidden = true
            self.homePlayer3View.isHidden = true
            
            self.awayPlayer1View.isHidden = true
            self.awayPlayer2View.isHidden = true
            self.awayPlayer3View.isHidden = true
        }
    }
    
    viewModel.isPaused.bindAndFire { [unowned self] in
        let title = $0 ? "Start" : "Pause"
        self.pauseButton.setTitle(title, for: .normal)
    }
    
    homePlayer1View.viewModel = viewModel.homePlayers[0]
    homePlayer2View.viewModel = viewModel.homePlayers[1]
    homePlayer3View.viewModel = viewModel.homePlayers[2]
    
    awayPlayer1View.viewModel = viewModel.awayPlayers[0]
    awayPlayer2View.viewModel = viewModel.awayPlayers[1]
    awayPlayer3View.viewModel = viewModel.awayPlayers[2]
}

The main difference is that you’ve modified your four dynamic properties and attached change listeners to each of them.

If you run your app now, toggling the Start/Pause button will start and pause the game timer, simulating time-outs during the game.

You’re almost there! The only remaining issue is that the score doesn’t update in the UI when you press the point buttons (the 1 and 2 points buttons).

This is because you haven’t propagated score changes from the underlying Game model object up to the ViewModel.

Open the Game model object for inspection. Examine its updateScore method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
fileprivate func updateScore(_ score: UInt, withScoringPlayer player: Player) {
    if isFinished || score == 0 {
        return
    }
    
    if homeTeam.containsPlayer(player) {
        homeTeamScore += score
    } else {
        assert(awayTeam.containsPlayer(player))
        awayTeamScore += score
    }
    
    if checkIfFinished() {
        isFinished = true
    }
    
    NotificationCenter.default.post(name: Notification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: self)
}

This method performs two crucial actions. First, it sets the isFinished property to true if the game has ended based on the scores of both teams. Second, it posts a notification indicating that the score has changed. You’ll listen for this notification in GameScoreboardEditorViewModelFromGame and update the dynamic score value in the notification handler method.

Add the following line at the bottom of the initWithGame method (don’t forget to call super.init() to avoid errors):

1
2
super.init()
subscribeToNotifications()

Below the initWithGame method, add the deinit method to ensure proper cleanup and prevent crashes related to the NotificationCenter.

1
2
3
deinit {
    unsubscribeFromNotifications()
}

Finally, implement these methods. Add the following code section right after the deinit method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// MARK: Notifications (Private)

fileprivate func subscribeToNotifications() {
    NotificationCenter.default.addObserver(self,
                                           selector: #selector(gameScoreDidChangeNotification(_:)),
                                           name: NSNotification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification),
                                           object: game)
}

fileprivate func unsubscribeFromNotifications() {
    NotificationCenter.default.removeObserver(self)
}

@objc fileprivate func gameScoreDidChangeNotification(_ notification: NSNotification){
    self.score.value = GameScoreboardEditorViewModelFromGame.scorePretty(for: game)
    
    if game.isFinished {
        self.isFinished.value = true
    }
}

Run the app now and try clicking the player views to change scores. Because you’ve connected the dynamic score and isFinished properties in the ViewModel to the View, everything should work correctly when the score value changes within the ViewModel.

Further Enhancements

While this tutorial covers the fundamentals, there’s always room for improvement.

For instance, the app currently doesn’t stop the timer automatically when the game ends (when one team reaches 15 points). It only hides the player views.

Feel free to experiment with the app and consider adding features like:

  • A “game creator” view to set up new games, assign team names, add players, and create Game objects that can be used to present GameScoreboardEditorViewController.
  • A “game list” view using a UITableView to display multiple ongoing games with relevant details in each table cell. Selecting a cell could then present the GameScoreboardEditorViewController with the corresponding Game object.

The existing GameLibrary provides a starting point. Remember to pass a reference to this library to the ViewModel objects in their initializers. For instance, the “game creator” ViewModel would need access to the GameLibrary to insert newly created Game objects. Similarly, the “game list” ViewModel would need this reference to fetch all games from the library for display in the UITableView.

The underlying principle is to encapsulate all non-UI logic within the ViewModel, allowing the View to focus solely on presenting prepared data.

Moving Forward

Once you’re comfortable with MVVM, you can explore further improvements by incorporating Uncle Bob’s Clean Architecture rules.

For additional insights, consider reading this three-part tutorial on Android architecture:

Although the examples are written in Java (for Android), the concepts translate well to Swift. You’ll gain valuable ideas on refactoring code within ViewModel objects to eliminate dependencies on iOS-specific modules like UIKit or CoreLocation.

Hiding these iOS modules behind pure NSObjects enhances code reusability.

MVVM is a robust architectural pattern well-suited for most iOS apps. Consider giving it a try in your next project or even experiment with it when creating new UIViewControllers in your existing projects.

Licensed under CC BY-NC-SA 4.0