Radu Dan
MVC Banner

Architecture Series - Model View Controller (MVC)

Motivation

Before starting to develop an iOS app, we have to think of the structure of the project. We need to consider how we add those pieces of code together so they make sense later on - when we come back and revisit a part of the app - and how to form a known “language” with the other developers.

We are kicking off with a series of articles where we take one application through the most known patterns of iOS development.
We will check build times, pros and cons of each pattern, but most important we will see the actual implementation and the source code.

If you just want to see the code, feel free to skip this post. The code is available open source here.

Why an architecture pattern for your iOS app

The most important thing to consider is to have an app that can be maintainable. You know the View goes there, this View Controller should do X and not Y. And more important the others know that too.

Here are some advantages of choosing a good architecture pattern:

  • Easier to maintain
  • Easier to test the business logic
  • Develop a common language with the other teammates
  • Separate the responsibility of your entities
  • Less bugs

Defining the requirements

Given an iOS application with 6-7 screens, we are going to develop it using the most popular architecture patterns from the iOS world: MVC, MVVM, MVP, VIPER, VIP and Coordinators.

The demo app is called Football Gather and is a simple way of friends to track score of their amateur football matches.

Main features

  • Ability to add players in the app
  • You can assign teams to the players
  • Edit players
  • Set countdown timer for matches

Screen Mockups

Football Gather Mockups

Backend

The app is powered by a web app developed in Vapor web framework. You can check the app here (Vapor 3 initial article) and here (Migrating to Vapor 4).

What is MVC

Probably the most known architecture pattern in the world.

There are three components: Model, View and Controller.

Model

  • Contains all your data classes, helpers, networking code
  • All data specific to your app and defines the logic that processes that data
  • In our app, the model represents what is found in the Utils, Storage and Networking groups
  • It can have a 1:M and M:M relationships with other model objects, as we have players with gathers (M:M) or user with players / gathers (1:M)
  • It should not communicate directly with the View and should not care about the user interface

Communication

  • When something happens in the view layer, for example when the user initiates an action, it is communicated to the model through the Controller
  • When the model is changed, for example when new data is available, the model notifies the Controller.

View

  • Represents components of what users see on the screen
  • Responds to user actions
  • The purpose of the view is to show data from the Model and to make it available for user interactions
  • Key Apple frameworks: UIKit, AppKit
  • Examples from our app: LoadingView, EmptyView, PlayerTableViewCell, ScoreStepper

Communication

  • Views can’t communicate directly with the Model, everything is done through the Controller

Controller

  • This is the core layer of MVC
  • It takes care of View updates and mutates the Model
  • It takes care of Model updates and updates the View
  • Controllers can have setup methods or tasks to manage the life cycles of other objects

Communication

  • Can communicate with both layers, Model and View
  • Controllers interprets user actions and triggers changes to the data via Model layer
  • When data changes, it makes sure those changes are communicated to the user interface, updating the View

Different flavours of MVC

The traditional MVC is different from Cocoa’s MVC. View and Model layers could communicate between each other.
The View is stateless and rendered by the Controller once the Model has been updated.
It was introduced into Smalltalk-79 and has been created on top of several design patterns: composite, strategy and observer.

Composite

The view objects in an application are actually a composite of nested views that work together in a coordinated fashion (that is, the view hierarchy). These display components range from a window to compound views, such as a table view, to individual views, such as buttons. User input and display can take place at any level of the composite structure.

Think about UIView’s hierarchy. Views are main components of user’s interface. They can contain other subviews.
For example, in our app, LoginViewController has the main view that contains a bunch of stack views and inside them are the text fields for entering the username and password and the login button.

Strategy

A controller object implements the strategy for one or more view objects. The view object confines itself to maintaining its visual aspects, and it delegates to the controller all decisions about the application-specific meaning of the interface behavior.

Observer

A model object keeps interested objects in an application—usually view objects—advised of changes in its state.

The main disadvantage with the old MVC is that all three layers are tightly coupled. Is hard to test, maintain them and even reuse some of the logic.

How and when to use MVC

It depends.

If you do it right, you can use it in every app. There is no a definitive answer, yes or no, it depends on your app, teams, organisation, growth of the project, skills of the developers, deadlines etc.

However, there are some vulnerable points you should consider:

  • As you can see, the Controller is in the center of this architecture pattern. It has a strong couple with the View and the Model layers.
  • The Controller might turn into the well known Massive View Controller
  • It is harder to test

There are ways to tackle the points mentioned above. One of them is to split the View Controller into mini-ViewControllers, where you have a big Container/Parent View Controller that acts as a coordinator and each zone of the app is handled by a different or a child View Controller.

Why should you use MVC:

  • It is recommended by Apple and used in their frameworks (e.g. UIKit)
  • A lot of the developers know this pattern and is easy to collaborate with them
  • You write code faster than in other architecture patterns

Applying to our code

In FootballGather we implement each screen as a View Controller:

LoginViewController

Login Screen

Description

  • landing page where users can login with their credentials or create new users

UI elements

  • usernameTextField - this is the text field where users enter their username
  • passwordTextField - secure text field for entering passwords
  • rememberMeSwitch - is an UISwitch for saving the username in Keychain after login and autopopulate the field next time we enter the app
  • loadingView - is used to show an indicator while a server call is made

Services

  • loginService - used to call the Login API with the entered credentials
  • usersService - used to call the Register API, creating a new user

As we can see, this class has three major functions: login, register and remember my username. Jumping to next screen is done via performSegue.

Code snippet


@IBAction private func login(_ sender: Any) {
   // This is the minimum validation that we do, we check if the fields are empty. If yes, we display an alert to the user.
   guard let userText = usernameTextField.text, userText.isEmpty == false,
       let passwordText = passwordTextField.text, passwordText.isEmpty == false else {
           AlertHelper.present(in: self, title: "Error", message: "Both fields are mandatory.")
           return
   }

   // Adds the loading view while the Network call is performed
   showLoadingView()

   // Creates the request model
   let requestModel = UserRequestModel(username: userText, password: passwordText)

   // Calls the Login webservice API and handles the Result
   loginService.login(user: requestModel) { [weak self] result in
       guard let self = self else { return }

       DispatchQueue.main.async {
           self.hideLoadingView()

           switch result {
           case .failure(let error):
               // An error has occured. Make sure the user is informed
               AlertHelper.present(in: self, title: "Error", message: String(describing: error))

           case .success(_):
               // Hides the loading view, stores rememberMe option in Keychain or cleares the existing one and performs the segue so we advance to next screen.
               self.handleSuccessResponse()
           }
       }
   }

PlayerListViewController

Main Screen Selection Screen

Description

  • shows the players for the logged in user. Consists of a main table view, each player is displayed in a separated row.

UI elements

  • playerTableView - The table view that displays players
  • confirmOrAddPlayersButton - Action button from the bottom of the view, that can either correspond to an add player action or confirms the selected players
  • loadingView - is used to show an indicator while a server call is made
  • emptyView - Shown when the user hasn’t added any players
  • barButtonItem - The top right button that can have different states based on the view mode we are in. Has the title “Cancel” when we go into selection mode to choose the players we want for the gather or “Select” when we are in view mode.

Services

  • playersService - Used to retrieve the list of players and to delete a player

Models

  • players - An array of players created by the user. This are the rows we see in playerTableView
  • selectedPlayersDictionary - A cache dictionary that stores the row index of the selected player as key and the selected player as value.

If you open up Main.storyboard you can see that from this view controller you can perform three segues

  • ConfirmPlayersSegueIdentifier - After you select what players you want for your gather, you go to a confirmation screen where you assign the teams they will be part of.
  • PlayerAddSegueIdentifier - Goes to a screen where you can create a new player
  • PlayerDetailSegueIdentifier - Opens a screen where you can see the details of the player

We have the following function to retrieve the model for this View Controller.


    // Performs a GET request to Players API to retrieve the list of the players.
    private func loadPlayers() {
       // UI setup methods, show Network activity indicator
       view.isUserInteractionEnabled = false

       // Perfoms the GET request
       playersService.get { [weak self] (result: Result<[PlayerResponseModel], Error>) in
           DispatchQueue.main.async {
               self?.view.isUserInteractionEnabled = true

               switch result {
               case .failure(let error):
                   // In case of failures, present an alert to the user.
                   self?.handleServiceFailures(withError: error)

               case .success(let players):
                   // Server returned a successful response with the array of players
                   self?.players = players
                   self?.handleLoadPlayersSuccessfulResponse()
               }
           }
       }
    }

And if we want to delete one player we do the following:


    // Table delegate that asks the data source to commit the insertion or deletion of a specified row in the receiver.
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
       // We just use it for deletion and not editing
       guard editingStyle == .delete else { return }

       // Show a confirmation alert if we are sure to delete the player
       let alertController = UIAlertController(title: "Delete player", message: "Are you sure you want to delete the selected player?", preferredStyle: .alert)
       let confirmAction = UIAlertAction(title: "Delete", style: .destructive) { [weak self] _ in
           self?.handleDeletePlayerConfirmation(forRowAt: indexPath)
       }
       alertController.addAction(confirmAction)

       let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
       alertController.addAction(cancelAction)

       present(alertController, animated: true, completion: nil)
    }

    private func handleDeletePlayerConfirmation(forRowAt indexPath: IndexPath) {
       showLoadingView()

       // Try to delete the player from the server and if successful, delete it
       requestDeletePlayer(at: indexPath) { [weak self] result in
           guard result, let self = self else { return }

           // Deletes the player from the data source and performs the table row animations.
           self.playerTableView.beginUpdates()
           self.players.remove(at: indexPath.row)
           self.playerTableView.deleteRows(at: [indexPath], with: .fade)
           self.playerTableView.endUpdates()

           if self.players.isEmpty {
               self.showEmptyView()
           }
       }
    }

The service call is presented below:


    private func requestDeletePlayer(at indexPath: IndexPath, completion: @escaping (Bool) -> Void) {
        let player = players[indexPath.row]
        var service = playersService

        service.delete(withID: ResourceID.integer(player.id)) { [weak self] result in
           DispatchQueue.main.async {
               switch result {
               case .failure(let error):
                   self?.handleServiceFailures(withError: error)
                   completion(false)

               case .success(_):
                   completion(true)
               }
           }
   }

PlayerAddViewController

Add Player Screen

Description

  • This screen is used for creating a player.

UI Elements

  • playerNameTextField - Used to enter the name of the player
  • doneButton - Bar button item that is used to confirm the player to be created and initiates a service call
  • loadingView - Is used to show an indicator while a server call is made

Services

  • We use the StandardNetworkService that points to /api/players. To add players, we initiate a POST request.

Code snippet


          private func createPlayer(_ player: PlayerCreateModel, completion: @escaping (Bool) -> Void) {
                  let service = StandardNetworkService(resourcePath: "/api/players", authenticated: true)
                  service.create(player) { result in
                      if case .success(_) = result {
                          completion(true)
                      } else {
                          completion(false)
                      }
                  }
          }

PlayerDetailViewController

Player Details Screen

Description

  • maps a screen that shows the details of a player (name, age, position, skill and favourite team)

UI elements

  • playerDetailTableView - A tableview that displays the details of the player.

Model

  • player - The model of the player as PlayerResponseModel

We have no services in this ViewController. A request to update an information of player is received fromPlayerEditViewController and passed to PlayerListViewController through delegation.

The sections are made with a factory pattern:


       // There are three sections: Personal, Play and Likes
       private func makeSections() -> [PlayerSection] {
           return [
               PlayerSection(
                   title: "Personal",
                   rows: [
                       PlayerRow(title: "Name",
                                 value: self.player?.name ?? "",
                                 editableField: .name),
                       PlayerRow(title: "Age",
                                 value: self.player?.age != nil ? "\(self.player!.age!)" : "",
                                 editableField: .age)
                   ]
               ),
               PlayerSection(
                   title: "Play",
                   rows: [
                       PlayerRow(title: "Preferred position",
                                 value: self.player?.preferredPosition?.rawValue.capitalized ?? "",
                                 editableField: .position),
                       PlayerRow(title: "Skill",
                                 value: self.player?.skill?.rawValue.capitalized ?? "",
                                 editableField: .skill)
                   ]
               ),
               PlayerSection(
                   title: "Likes",
                   rows: [
                       PlayerRow(title: "Favourite team",
                                 value: self.player?.favouriteTeam ?? "",
                                 editableField: .favouriteTeam)
                   ]
               )
           ]
       }

PlayerEditViewController

Edit Position Screen Edit Skill Screen Edit Age Screen

Description

  • Edits a player information.

UI Elements

  • playerEditTextField - The field that is filled with the player’s detail we want to edit
  • playerTableView - We wanted to have a similar behaviour and UI as we have in iOS General Settings for editing a details. This table view has either one row with a text field or multiple rows with a selection behaviour.
  • loadingView - is used to show an indicator while a server call is made
  • doneButton - An UIBarButtonItem that performs the action of editing.

Services

  • Update Player API, used as a StandardNetworkService:

    private func updatePlayer(_ player: PlayerResponseModel, completion: @escaping (Bool) -> Void) {
       var service = StandardNetworkService(resourcePath: "/api/players", authenticated: true)
       service.update(PlayerCreateModel(player), resourceID: ResourceID.integer(player.id)) { result in // players have an ID that is Int
           if case .success(let updated) = result {
               completion(updated)
           } else {
               completion(false)
           }
       }
   }

Models

  • viewType - An enum that can be .text (for player details that are entered via keyboard) or .selection (for player details that are selected by tapping one of the cells, for example the preferred position).
  • player - The player we want to edit.
  • items - An array of strings corresponding to all possible options for preferred positions or skill. This array is nil when a text entry is going to be edited.

ConfirmPlayersViewController

Confirm Players Screen

Description

  • Before reaching the Gather screen we want to put the players in the desired teams

UI elements

  • playerTableView - A table view split in three sections (Bench, Team A and Team B) that shows the selected players we want for the gather.
  • startGatherButton - Initially disabled, when tapped triggers an action to perform the Network API calls required to start the gather and at last, it pushes the next screen.
  • loadingView - is used to show an indicator while a server call is made.

Services

  • Create Gather - Adds a new gather by making a POST request to /api/gathers.
  • Add Player to Gather - After we are done with selecting teams for our players, we add them to the gather by doing a POST request to api/gathers/{gather_id}/players/{player_id}.

Models

  • playersDictionary - Each team has an array of players, so the dictionary has the teams as keys (Team A, Team B or Bench) and for values we have the selected players (array of players).

When we are done with the selection (UI), a new gather is created and each player is assigned a team.


    @IBAction func startGatherAction(_ sender: Any) {
        showLoadingView()
        // Authenticated request to create a new gather
        createGather { [weak self] uuid in
            guard let self = self else { return }
            guard let gatherUUID = uuid else {
                self.handleServiceFailure()
                return
            }
            
            // Each player is added to the gather
            self.addPlayersToGather(havingUUID: gatherUUID)
        }
    }

The for loop to add players is presented below:


    private func addPlayersToGather(havingUUID gatherUUID: UUID) {
        let players = self.playerTeamArray
        // [1] Using a DispatchGroup so we wait all requests to finish
        let dispatchGroup = DispatchGroup()
        var serviceFailed = false
        
        players.forEach { playerTeamModel in
            dispatchGroup.enter()
            
            self.addPlayer(
                playerTeamModel.player,
                toGatherHavingUUID: gatherUUID,
                team: playerTeamModel.team,
                completion: { playerWasAdded in
                    if !playerWasAdded {
                        serviceFailed = true
                    }
                    
                    dispatchGroup.leave()
            })
        }
        
        // [2] All requests finished, now update the UI
        dispatchGroup.notify(queue: DispatchQueue.main) {
            self.hideLoadingView()
            
            if serviceFailed {
                self.handleServiceFailure()
            } else {
                self.performSegue(withIdentifier: SegueIdentifiers.gather.rawValue,
                                  sender: GatherModel(players: players, gatherUUID: gatherUUID))
            }
        }
    }

GatherViewController

Gather Screen

Description

  • This is the core screen of the application, where you are in the gather mode and start / pause or stop the timer and in the end, finish the match.

UI elements

  • playerTableView - Used to display the players in gather, split in two sections: Team A and Team B.
  • scoreLabelView - A view that has two labels for displaying the score, one for Team A and the other one for Team B.
  • scoreStepper - A view that has two steppers for the teams.
  • timerLabel - Used to display the remaining time in the format mm:ss.
  • timerView - An overlay view that has a UIPickerView to choose the time of the gather.
  • timePickerView - The picker view with two components (minutes and seconds) for selecting the gather’s time.
  • actionTimerButton - Different state button that manages the countdown timer (resume, pause and start).
  • loadingView - is used to show an indicator while a server call is made.

Services

  • Update Gather - when a gather is ended, we do a PUT request to update the winner team and the score

Models

  • GatherTime - A tuple that has minutes and seconds as Int.
  • gatherModel - Contains the gather ID and an array of player team model (the player response model and the team he belongs to). This is created and passed from ConfirmPlayersViewController.
  • timer - Used to countdown the minutes and seconds of the gather.
  • timerState - Can have three states stopped, running and paused. We observer when one of the values is set so we can change the actionTimerButton’s title accordingly. When it’s paused the button’s title will be Resume. When it’s running the button’s title will be Pause and Start when the timer is stopped.

When the actionTimerButton is tapped, we verify if we want to invalidate or start the timer:


    @IBAction func actionTimer(_ sender: Any) {
        // [1] Check if the user selected a time more than 1 second
        guard selectedTime.minutes > 0 || selectedTime.seconds > 0 else {
            return
        }
        
        switch timerState {
        // [2] The timer was in a stopped or paused state. Now it becomes in a running state.
        case .stopped, .paused:
            timerState = .running
        // [3] If it’s running, we pause it. For cancelling we have a different IBAction.
        case .running:
            timerState = .paused
        }
        
        if timerState == .paused {
            // [4] Stop the timer
            timer.invalidate()
        } else {
            // [5] Start the timer and call each second the selector updateTimer.
            timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)       
        }
    }

To cancel a timer we have the following action implemented:


    @IBAction func cancelTimer(_ sender: Any) {
        // [1] Set the state
        timerState = .stopped
        timer.invalidate()
        // [2] Set to 10 minutes
        selectedTime = Constants.defaultTime
        timerView.isHidden = true
    }

The selector updateTimer is called each second:



    @objc func updateTimer(_ timer: Timer) {
        // [1] Before substracting a second, we verify the current timer state. If the seconds are 0, we substract the minutes.
        if selectedTime.seconds == 0 {
            selectedTime.minutes -= 1
            selectedTime.seconds = 59
        } else {
            selectedTime.seconds -= 1
        }
        
        // [2] If timer reached out to 0, we stop it.
        if selectedTime.seconds == 0 && selectedTime.minutes == 0 {
            timerState = .stopped
            timer.invalidate()
        }
    }

Before ending a gather we check the winner team:


        guard let scoreTeamAString = scoreLabelView.teamAScoreLabel.text,
            let scoreTeamBString = scoreLabelView.teamBScoreLabel.text,
            let scoreTeamA = Int(scoreTeamAString),
          // [1] Get the score from the labels and convert it to Ints.
            let scoreTeamB = Int(scoreTeamBString) else {
                return
        }
        
        //[2] Format of the score
        let score = "\(scoreTeamA)-\(scoreTeamB)"
        
        var winnerTeam: String = "None"
        if scoreTeamA > scoreTeamB {
            winnerTeam = "Team A"
        } else if scoreTeamA < scoreTeamB {
            winnerTeam = "Team B"
        }

        // [3] Set the winner team, by default being None.
        let gather = GatherCreateModel(score: score, winnerTeam: winnerTeam)

And the service call:


    private func updateGather(_ gather: GatherCreateModel, completion: @escaping (Bool) -> Void) {
        guard let gatherModel = gatherModel else {
            completion(false)
            return
        }
        
        // [1] We are using the StandardNetworkService where we pass the UUID of the gather and the update model (winner team and score)
        var service = StandardNetworkService(resourcePath: "/api/gathers", authenticated: true)
        service.update(gather, resourceID: ResourceID.uuid(gatherModel.gatherUUID)) { result in
            if case .success(let updated) = result {
                completion(updated)
            } else {
                completion(false)
            }
        }
    }

The private method updateGather is called from endGather:


        let gather = GatherCreateModel(score: score, winnerTeam: winnerTeam)
                  
        showLoadingView()
        updateGather(gather) { [weak self] gatherWasUpdated in
            guard let self = self else { return }
            
            DispatchQueue.main.async {
                self.hideLoadingView()
                
                if !gatherWasUpdated {
                  // [1] The server call failed, make sure we show an alert to the user.
                    AlertHelper.present(in: self, title: "Error update", message: "Unable to update gather. Please try again.")
                } else {
                    guard let playerViewController = self.navigationController?.viewControllers.first(where: { $0 is PlayerListViewController }) as? PlayerListViewController else {
                        return
                    }
                    
                  // [2] The PlayerListViewController is in a selection mode state. We make sure we turn it back to .list.
                    playerViewController.toggleViewState()
                  // [3] we don’t need the previous screen, PlayerConfirmViewController
                    self.navigationController?.popToViewController(playerViewController, animated: true)
                }
            }
        }

Testing our business logic

We saw a first iteration of applying MVC to the demo app FootbalGather. Of course, we can refactor the code and make it better and decouple some of the logic, split it into different classes, but for the sake of the exercise we are going to keep this version of the codebase.

Let’s see how we can write unit tests for our classes. We are going to exemplify for GatherViewController and try to reach close to 100% code coverage.

First, we see GatherViewController is part of Main storyboard. To make our lives easier, we use an identifier and instantiate it with the method storyboard.instantiateViewController. Let’s use the setUp method for this logic:


      final class GatherViewControllerTests: XCTestCase {
    
          var sut: GatherViewController!
          
          override func setUp() {
              super.setUp()
              
              let storyboard = UIStoryboard(name: "Main", bundle: nil)
              if let viewController = storyboard.instantiateViewController(identifier: "GatherViewController") as? GatherViewController {
                  sut = viewController
                  sut.gatherModel = gatherModel
                  _ = sut.view
              } else {
                  XCTFail("Unable to instantiate GatherViewController")
              }
          }


      //…
      }

For our first test, we verify all outlets are not nil:


    func testOutlets_whenViewControllerIsLoadedFromStoryboard_areNotNil() {
        XCTAssertNotNil(sut.playerTableView)
        XCTAssertNotNil(sut.scoreLabelView)
        XCTAssertNotNil(sut.scoreStepper)
        XCTAssertNotNil(sut.timerLabel)
        XCTAssertNotNil(sut.timerView)
        XCTAssertNotNil(sut.timePickerView)
        XCTAssertNotNil(sut.actionTimerButton)
    }

Now let’s see if viewDidLoad is called. The title is set and some properties are configured. We verify the public parameters:


    func testViewDidLoad_whenViewControllerIsLoadedFromStoryboard_setsVariables() {
        XCTAssertNotNil(sut.title)
        XCTAssertTrue(sut.timerView.isHidden)
        XCTAssertNotNil(sut.timePickerView.delegate)
    }

The variable timerView is a pop-up custom view where users set their match timer.

Moving forward let’s unit test our table view methods:


    func testNumberOfSections_whenGatherModelIsSet_returnsTwoTeams() {
        XCTAssert(sut.playerTableView?.numberOfSections == Team.allCases.count - 1)
    }

We have just two teams: Team A and Team B. The Bench team is not visible and not part of this screen.


    func testTitleForHeaderInSection_whenSectionIsTeamAAndGatherModelIsSet_returnsTeamATitleHeader() {
          let teamASectionTitle = sut.tableView(sut.playerTableView, titleForHeaderInSection: 0)
          XCTAssertEqual(teamASectionTitle, Team.teamA.headerTitle)
    }
      
    func testTitleForHeaderInSection_whenSectionIsTeamBAndGatherModelIsSet_returnsTeamBTitleHeader() {
          let teamBSectionTitle = sut.tableView(sut.playerTableView, titleForHeaderInSection: 1)
          XCTAssertEqual(teamBSectionTitle, Team.teamB.headerTitle)
    }

Our tableview should have two sections with both header titles being set to the team names (Team A and Team B).

For checking the number of rows, we inject a mocked gather model:


    private let gatherModel = ModelsMockFactory.makeGatherModel(numberOfPlayers: 4)


    static func makeGatherModel(numberOfPlayers: Int, gatherUUID: UUID = ModelsMock.gatherUUID) -> GatherModel {
        let allSkills = PlayerSkill.allCases
        let allPositions = PlayerPosition.allCases
        
        var playerTeams: [PlayerTeamModel] = []
        
        (1...numberOfPlayers).forEach { index in
            let skill = allSkills[Int.random(in: 0..<allSkills.count)]
            let position = allPositions[Int.random(in: 0..<allPositions.count)]
            let team: Team = index % 2 == 0 ? .teamA : .teamB
            
            let playerResponseModel = makePlayerResponseModel(id: index, name: "Player \(index)", age: 20 + index, favouriteTeam: "Fav team \(index)", skill: skill, preferredPosition: position)
            let playerTeamModel = PlayerTeamModel(team: team, player: playerResponseModel)
            
            playerTeams.append(playerTeamModel)
        }
        
        return GatherModel(players: playerTeams, gatherUUID: gatherUUID)
    }

And the unit tests for verifying the number of rows in sections:


    func testNumberOfRowsInSection_whenTeamIsA_returnsNumberOfPlayersInTeamA() {
        let expectedTeamAPlayersCount = gatherModel.players.filter { $0.team == .teamA }.count
        XCTAssertEqual(sut.playerTableView.numberOfRows(inSection: 0), expectedTeamAPlayersCount)
    }
    
    func testNumberOfRowsInSection_whenTeamIsB_returnsNumberOfPlayersInTeamB() {
        let expectedTeamBPlayersCount = gatherModel.players.filter { $0.team == .teamB }.count
        XCTAssertEqual(sut.playerTableView.numberOfRows(inSection: 1), expectedTeamBPlayersCount)
    }

Nil scenario when the section is invalid.


    func testNumberOfRowsInSection_whenGatherModelIsNil_returnsZero() {
        sut.gatherModel = nil
        XCTAssertEqual(sut.tableView(sut.playerTableView, numberOfRowsInSection: -1), 0)
    }

For displaying the player details we use the normal table view cells, setting the textLabel with the player’s name and the detailTextLabel with the player’s preferred position.

Let’s verify these properties are set:


    func testCellForRowAtIndexPath_whenSectionIsTeamA_setsCellDetails() {
        let indexPath = IndexPath(row: 0, section: 0)
        let playerTeams = gatherModel.players.filter({ $0.team == .teamA })
        let player = playerTeams[indexPath.row].player
        
        let cell = sut.playerTableView.cellForRow(at: indexPath)
        
        XCTAssertEqual(cell?.textLabel?.text, player.name)
        XCTAssertEqual(cell?.detailTextLabel?.text, player.preferredPosition?.acronym)
    }
    
    func testCellForRowAtIndexPath_whenSectionIsTeamB_setsCellDetails() {
        let indexPath = IndexPath(row: 0, section: 1)
        let playerTeams = gatherModel.players.filter({ $0.team == .teamB })
        let player = playerTeams[indexPath.row].player
        
        let cell = sut.playerTableView.cellForRow(at: indexPath)
        
        XCTAssertEqual(cell?.textLabel?.text, player.name)
        XCTAssertEqual(cell?.detailTextLabel?.text, player.preferredPosition?.acronym)
    }

Great! We’ve now successfully tested our model. It wasn’t that hard, but we had to mock stuff, use storyboards and check the table view delegate and data source, a lot of UI stuff for verifying the business logic.

We continue with the pickerView methods.


    func testPickerViewNumberOfComponents_returnsAllCountDownCases() {
        XCTAssertEqual(sut.timePickerView.numberOfComponents, GatherViewController.GatherCountDownTimerComponent.allCases.count)
    }

The only way to verify the number of components is if we make the enum GatherCountDownTimerComponent public.

Testing the number of rows in component is somehow similar with what we did for our players tableView.


    func testPickerViewNumberOfRowsInComponent_whenComponentIsMinutes_returns60() {
        let minutesComponent = GatherViewController.GatherCountDownTimerComponent.minutes.rawValue
        let numberOfRows = sut.pickerView(sut.timePickerView, numberOfRowsInComponent: minutesComponent)
        
        XCTAssertEqual(numberOfRows, 60)
    }
    
    func testPickerViewNumberOfRowsInComponent_whenComponentIsSecounds() {
        let secondsComponent = GatherViewController.GatherCountDownTimerComponent.seconds.rawValue
        let numberOfRows = sut.pickerView(sut.timePickerView, numberOfRowsInComponent: secondsComponent)
        
        XCTAssertEqual(numberOfRows, 60)
    }

The rows should equal with the number of minutes and seconds. And the title for rows:


    func testPickerViewTitleForRow_whenComponentIsMinutes_containsMin() {
        let minutesComponent = GatherViewController.GatherCountDownTimerComponent.minutes.rawValue
        let title = sut.pickerView(sut.timePickerView, titleForRow: 0, forComponent: minutesComponent)
        
        XCTAssertTrue(title!.contains("min"))
    }
    
    func testPickerViewTitleForRow_whenComponentIsSeconds_containsSec() {
        let secondsComponent = GatherViewController.GatherCountDownTimerComponent.seconds.rawValue
        let title = sut.pickerView(sut.timePickerView, titleForRow: 0, forComponent: secondsComponent)
        
        XCTAssertTrue(title!.contains("sec"))
    }

Let’s see how we test the IBActions.

The setTimer action configures the picker view (it sets the picker view components to the selected minutes and seconds. If not, it defaults to 10 minutes) and hides the timerView overlay.

The only public property we have is the timerView. We call setTimer and verify timerView is not hidden:


    func testSetTimer_whenActionIsSent_showsTimerView() {
        sut.setTimer(UIButton())
        XCTAssertFalse(sut.timerView.isHidden)
    }

We replicate the same logic for cancelTimer, but this time our timerView should be hidden:


    func testCancelTimer_whenActionIsSent_hidesTimerView() {
        sut.cancelTimer(UIButton())
        XCTAssertTrue(sut.timerView.isHidden)
    }

And the similar method for the overlay button from the timerView popup:


    func testTimerCancel_whenActionIsSent_hidesTimerView() {
        sut.timerCancel(UIButton())
        XCTAssertTrue(sut.timerView.isHidden)
    }

Things are getting harder and harder:


    func testTimerDone_whenActionIsSent_hidesTimerViewAndSetsMinutesAndSeconds() {
        sut.timerDone(UIButton())
        
        let minutes = sut.timePickerView.selectedRow(inComponent: GatherViewController.GatherCountDownTimerComponent.minutes.rawValue)
        let seconds = sut.timePickerView.selectedRow(inComponent: GatherViewController.GatherCountDownTimerComponent.seconds.rawValue)


        XCTAssertTrue(sut.timerView.isHidden)
        XCTAssertGreaterThan(minutes, 0)
        XCTAssertEqual(seconds, 0)
    }

We verify the timerView is hidden and the minutes and seconds are set. We don’t have access to the ViewController’s default time, so we use XCTAssertGreaterThan for minutes component.

Let’s see if our timer reacts to row changes:


    func testActionTimer_whenSelectedTimeIsZero_returns() {
        let minutesComponent = GatherViewController.GatherCountDownTimerComponent.minutes.rawValue
        let secondsComponent = GatherViewController.GatherCountDownTimerComponent.seconds.rawValue
        
        sut.timePickerView.selectRow(0, inComponent: minutesComponent, animated: false)
        sut.timePickerView.selectRow(0, inComponent: secondsComponent, animated: false)
        sut.timerDone(UIButton())
        sut.actionTimer(UIButton())
        
        XCTAssertEqual(sut.timePickerView.selectedRow(inComponent: minutesComponent), 0)
        XCTAssertEqual(sut.timePickerView.selectedRow(inComponent: secondsComponent), 0)
    }

In this test, we select the first rows, so the time will be 00:00. We then call timerDone method and actionTimer. It will hit the guard statement:


        guard selectedTime.minutes > 0 || selectedTime.seconds > 0 else {
            return
        }

Now, testing the happy path:


    func testActionTimer_whenSelectedTimeIsSet_updatesTimer() {
        // given
        let minutesComponent = GatherViewController.GatherCountDownTimerComponent.minutes.rawValue
        let secondsComponent = GatherViewController.GatherCountDownTimerComponent.seconds.rawValue
        
        sut.timePickerView.selectRow(0, inComponent: minutesComponent, animated: false)
        // set to 1 second
        sut.timePickerView.selectRow(1, inComponent: secondsComponent, animated: false)
        
        // initial state
        sut.timerDone(UIButton())
        XCTAssertEqual(sut.actionTimerButton.title(for: .normal), "Start")
        
        // when
        sut.actionTimer(UIButton())
        
        // then
        XCTAssertEqual(sut.actionTimerButton.title(for: .normal), "Pause")
        
        // make sure timer is resetted
        let exp = expectation(description: "Timer expectation")
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            XCTAssertEqual(self.sut.actionTimerButton.title(for: .normal), "Start")
            exp.fulfil()
        }
        
        waitForExpectations(timeout: 5, handler: nil)
    }

We check if actionTimerButton has the title Start when we are in an initial state. After we call actionTimer, the actionTimerButton should now have the title Pause (because the timer is counting and the match has started).

We set the seconds component to one. So, if we are dispatching after two seconds, we should have stopped, the timer should get invalidated and the actionTimerButton should be back to its initial state having the title Start.

To wait for the expectation fulfil, we use waitForExpectations, with a 5 seconds timeout.

Checking the switch between Resume and Pause is similar:


    func testActionTimer_whenTimerIsSetAndRunning_isPaused() {
        // given
        let sender = UIButton()
        let minutesComponent = GatherViewController.GatherCountDownTimerComponent.minutes.rawValue
        let secondsComponent = GatherViewController.GatherCountDownTimerComponent.seconds.rawValue
        
        sut.timePickerView.selectRow(0, inComponent: minutesComponent, animated: false)
        sut.timePickerView.selectRow(3, inComponent: secondsComponent, animated: false)
        
        // initial state
        sut.timerDone(sender)
        XCTAssertEqual(sut.actionTimerButton.title(for: .normal), "Start")
        sut.actionTimer(sender)
        
        // when
        let exp = expectation(description: "Timer expectation")
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.sut.actionTimer(sender)
            XCTAssertEqual(self.sut.actionTimerButton.title(for: .normal), "Resume")
            exp.fulfil()
        }
        
        waitForExpectations(timeout: 5, handler: nil)
    }

We set a longer period of time for seconds component (3 seconds). We call actionTimer to start and after one second, we call the function again. The match should be paused and the actionTimerButton should have the Resume title.

To check if the selector gets called, we can verify the timerLabel text:


    func testUpdateTimer_whenSecondsReachZero_decrementsMinuteComponent() {
        let sender = UIButton()
        let timer = Timer()
        let minutesComponent = GatherViewController.GatherCountDownTimerComponent.minutes.rawValue
        let secondsComponent = GatherViewController.GatherCountDownTimerComponent.seconds.rawValue
        
        sut.timePickerView.selectRow(1, inComponent: minutesComponent, animated: false)
        sut.timePickerView.selectRow(0, inComponent: secondsComponent, animated: false)
        sut.timerDone(sender)
        XCTAssertEqual(sut.timerLabel.text, "01:00")
        
        sut.updateTimer(timer)
        
        XCTAssertEqual(sut.timerLabel.text, "00:59")
    }

In this test we checked if the seconds are decremented when the minutes component is reaching zero.

Having access to the outlets, we can easily verify the stepperDidChangeValue delegates:


    func testStepperDidChangeValue_whenTeamAScores_updatesTeamAScoreLabel() {
        sut.scoreStepper.teamAStepper.value = 1
        sut.scoreStepper.teamAStepperValueChanged(UIButton())
        
        XCTAssertEqual(sut.scoreLabelView.teamAScoreLabel.text, "1")
        XCTAssertEqual(sut.scoreLabelView.teamBScoreLabel.text, "0")
    }
    
    func testStepperDidChangeValue_whenTeamBScores_updatesTeamBScoreLabel() {
        sut.scoreStepper.teamBStepper.value = 1
        sut.scoreStepper.teamBStepperValueChanged(UIButton())
        
        XCTAssertEqual(sut.scoreLabelView.teamAScoreLabel.text, "0")
        XCTAssertEqual(sut.scoreLabelView.teamBScoreLabel.text, "1")
    }
    
    func testStepperDidChangeValue_whenTeamIsBench_scoreIsNotUpdated() {
        sut.stepper(UIStepper(), didChangeValueForTeam: .bench, newValue: 1)
        
        XCTAssertEqual(sut.scoreLabelView.teamAScoreLabel.text, "0")
        XCTAssertEqual(sut.scoreLabelView.teamBScoreLabel.text, "0")
    }

Finally, the hardest and probably the most important method we have in GatherViewController is the endGather method. Here, we do a service call updating the gather model. We pass the winnerTeam and the score of the match.

It is a big method, does more than one thing and is private. (we use it as per example, functions should not be big and functions should do one thing!).

The responsibilities of this function are detailed below. endGather does the following:

  • gets the score from scoreLabelViews
  • computes the winner team by comparing the score
  • creates the GatherModel for the service call
  • shows a loading spinner
  • does the updateGather service call
  • hides the loading spinner
  • handles success and failure
  • for success, the view controller is popped to PlayerListViewController (this view should be in the stack)
  • for failure, it presents an alert

How we should test all of that? (Again, as best practice, this function should be splitted down into multiple functions).

Let’s take one step at a time.

Creating a mocked service and injecting it in our sut:


    private let session = URLSessionMockFactory.makeSession()
    private let resourcePath = "/api/gathers"
    private let appKeychain = AppKeychainMockFactory.makeKeychain()


    let endpoint = EndpointMockFactory.makeSuccessfulEndpoint(path: resourcePath)
    sut.updateGatherService = StandardNetworkService(session: session, urlRequest: AuthURLRequestFactory(endpoint: endpoint, keychain: appKeychain))

Testing the success handler. We use a protocol instead of the concrete class PlayerListViewController and we mock it in our test class:


    protocol PlayerListTogglable {
        func toggleViewState()
    }


    class PlayerListViewController: UIViewController, PlayerListTogglable { .. }


    private extension GatherViewControllerTests {
        final class MockPlayerTogglableViewController: UIViewController, PlayerListTogglable {
            weak var viewStateExpectation: XCTestExpectation?
            private(set) var viewState = true
            
            func toggleViewState() {
                viewState = !viewState
                viewStateExpectation?.fulfil()
            }
        }
    }

This should be part of a navigation controller:


    let playerListViewController = MockPlayerTogglableViewController()
    let window = UIWindow()
    let navController = UINavigationController(rootViewController: playerListViewController)
    window.rootViewController = navController
    window.makeKeyAndVisible()


    _ = playerListViewController.view
    XCTAssertTrue(playerListViewController.viewState)
        
   let exp = expectation(description: "Timer expectation")
   playerListViewController.viewStateExpectation = exp
        
   navController.pushViewController(sut, animated: false)

We check the initial viewState. It should be true.

The rest of the unit test is presented below:


    // mocked endpoint expects a 1-1 score
    sut.scoreLabelView.teamAScoreLabel.text = "1"
    sut.scoreLabelView.teamBScoreLabel.text = "1"


    let endpoint = EndpointMockFactory.makeSuccessfulEndpoint(path: resourcePath)
    sut.updateGatherService = StandardNetworkService(session: session, urlRequest: AuthURLRequestFactory(endpoint: endpoint, keychain: appKeychain))


    sut.endGather(UIButton())
    let alertController = (sut.presentedViewController as! UIAlertController)
    alertController.tapButton(atIndex: 0)
        
    waitForExpectations(timeout: 5) { _ in
        XCTAssertFalse(playerListViewController.viewState)
    }

We prepare our mocked endpoint and call endGather.
A confirmation dialog should appear on screen (UIAlertController). We tap OK to end our gather.
Using waitForExpectation we wait for the closure to be executed and we verify the success path; viewState should now be false.

Because endGather is a private method, we had to use the IBAction that calls this method. And for tapping on OK in the alert controller that was presented we had to use its private API:


    private extension UIAlertController {
        typealias AlertHandler = @convention(block) (UIAlertAction) -> Void


        func tapButton(atIndex index: Int) {
            guard let block = actions[index].value(forKey: "handler") else { return }
            
            let handler = unsafeBitCast(block as AnyObject, to: AlertHandler.self)
            handler(actions[index])
        }
    }

We don’t have the guarantee this unit test will work in future Swift versions. This is bad.

Key Metrics

Lines of code

File Number of lines of code
PlayerAddViewController 79
PlayerListViewController 387
PlayerDetailViewController 204
LoginViewController 126
PlayerEditViewController 212
GatherViewController 359
ConfirmPlayersViewController 260
TOTAL 1627

Unit Tests

Topic Data
Number of key classes (the ViewControllers) 7
Key Class GatherViewController
Number of Unit Tests 30
Code Coverage of Gathers feature 95.7%
How hard to write unit tests 5/5

Build Times

Build Time (sec)*
Average Build Time (after clean Derived Data & Clean Build) 9.78
Average Build Time 0.1
Average Unit Test Execution Time (after clean Derived Data & Clean Build) 12.78

* tests were run on an 8-Core Intel Core i9, MacBook Pro, 2019

Conclusion

MVC is the most known architecture pattern when it comes to iOS Development.

In this first article, we saw it in action, being applied to a small application. We used the naive approach, where each screen is represented by a View Controller.
In the real world, you should not follow this approach for a screen that has a lot actions, you should split the responsibilities. One solution is to use child view controllers.

We also took each screen of the app and explained what is their role, provided a small description, what UI elements are part of it, the model which the controller interacts with and we saw code snippets of the key methods.

Finally, we described the feel of writing unit tests of the key class, GatherViewController. It wasn't as easy as we hoped, we even had to use a private method of UIAlertController, which is bad practice. Apple might change the public API of this class in a future release, breaking our unit test

However, used well, MVC is really cool and really great when it comes to iOS app development.

We can't say much by looking at the key metrics yet, we will need to see how the other patterns do. We can guess that the number of lines of code and classes is much smaller in MVC. The other patterns introduce more layers, thus more lines of code.

Thanks for staying until the end! Some useful links below.

Useful Links

Item Series Links
The iOS App - Football Gather GitHub Repo Link
The web server application made in Vapor GitHub Repo Link
Vapor 3 - Backend APIs article link
Migrating to Vapor 4 article link
Model View Controller (MVC) GitHub Repo Link
Article Link
Model View ViewModel (MVVM) GitHub Repo Link
Article Link
Model View Presenter (MVP) GitHub Repo Link
Article Link
Coordinator Pattern - MVP with Coordinators (MVP-C) GitHub Repo Link
Article Link
View Interactor Presenter Entity Router (VIPER) GitHub Repo Link
Article Link
View Interactor Presenter (VIP) GitHub Repo Link
Article Link