Using models for consistency

Adding a question model involves quite some refactoring. First, we must define the Question model. Create a new Swift file named Question and add the following implementation to it:

import Foundation

struct Question: Codable {
    
    enum CodingKeys: String, CodingKey {
        case title
        case answerA = "answer_a"
        case answerB = "answer_b"
        case answerC = "answer_c"
        case correctAnswer = "correct_answer"
    }
    
    let title: String
    let answerA: String
    let answerB: String
    let answerC: String
    let correctAnswer: Int
}

If you followed along with Chapter 10, Fetching and Displaying Data From the Network, this model should look somewhat familiar. The Question struct conforms to the Codable protocol. Since not all property names match the JSON data, a custom mapping is provided. This is all that needs to be done in order to convert the JSON to a Question struct. In order to properly use this model with the dummy JSON data, a QuestionsFetchResponse will be added as well. This struct also conforms to the Codable protocol and encapsulates the questions as follows:

import Foundation

struct QuestionsFetchResponse: Codable {
    let questions: [Question]
}

Now that the Question model and the response container are in place, a couple of changes must be made to the existing code. First of all, the typealias in the TriviaApiLoading protocol should be modified as follows:

typealias QuestionsFetchedCallback = (Data) -> Void

Next, update the implementation of the TriviaAPI for the URLSession callback in loadTriviaQuestions(callback:) as follows:

URLSession.shared.dataTask(with: url) { data, response, error in
    guard let data = data
        else { return }
    
    callback(data)
}

The QuestionsLoadedCallback typealias should be updated to the following definition:

typealias QuestionsLoadedCallback = ([Question]) -> Void

And lastly, the implementation for loadQuestions(callback:) should be updated as follows:

func loadQuestions(callback: @escaping QuestionsLoadedCallback) {
    apiProvider.loadTriviaQuestions { data in
        
        let decoder = JSONDecoder()
        guard let questionResponse = try? decoder.decode(QuestionsFetchResponse.self, from: data)
            else { return }
        
        callback(questionResponse.questions)
    }
}

This wraps up the changes for the API. However, there still is some refactoring we must do in the view controllers. Rename the triviaJSON property on LoadTriviaViewController to the following:

var questionsArray: [Question]?

Make sure you replace all occurrences of triviaJSON to the new questions array. Also, make sure you change the following line in prepare(for:sender:):                      

questionViewController.triviaJSON = triviaJSON

Change this line to:

questionViewController.questionsArray = questionsArray

In QuestionViewController change the type of questionsArray to [Question] and remove the triviaJSON property. At this point, you can clear all of the JSON-related code from the guards in this class. You should be able to do this on your own since the compiler should guide you with errors. If you get stuck, look at the finished project in the Git repository.

By now, you should be able to run the tests and they should pass. To run your tests, click the Product menu item and select Test. Alternatively, press Cmd + U to run your tests. The tests run fine, but currently we haven't made sure that all of our questions in the JSON data have been converted to Question models. To make sure this worked, we'll load the JSON file in our test case, count the number of questions in the JSON file, and assert that it matches the number of questions in the callback.

Update the testLoadQuestions() method as shown in the following code snippet:

func testLoadQuestions() {
    let apiProvider = MockTriviaAPI()
    let questionsLoader = QuestionsLoader(apiProvider: apiProvider)
    let questionsLoadedExpectation = expectation(description: "Expected the questions to be loaded")
    questionsLoader.loadQuestions { questions in
        guard let filename = Bundle(for: LoadQuestionsTest.self).path(forResource: "TriviaQuestions", ofType: "json"),
            let triviaString = try? String(contentsOfFile: filename),
            let triviaData = triviaString.data(using: .utf8),
            let jsonObject = try? JSONSerialization.jsonObject(with: triviaData, options: []),
            let triviaJSON = jsonObject as? JSON,
            let jsonQuestions = triviaJSON["questions"] as? [JSON]
            else { return }
        
        XCTAssert(questions.count > 0, "More than 0 questions should be passed to the callback")
        XCTAssert(jsonQuestions.count == questions.count, "Number of questions in json must match the number of questions in the callback.")
        
        questionsLoadedExpectation.fulfill()
    }
    
    waitForExpectations(timeout: 5, handler: nil)
}

We use the same code as the code that was used before in the fake API to load the JSON file. Once the JSON is loaded, we use XCTAssert to make sure that more than zero questions were passed to the callback and that the number of questions in the JSON file matches the number of questions that were loaded.

XCTAssert takes a Boolean expression and a description. If the assertion fails, the description is shown. Adding good descriptions will help you to easily figure out which assertion in your test has made your test fail.

This new test method is a small addition to our test but has huge consequences. By improving our test, we have improved the quality of our app because we are now sure that our question loader correctly transforms JSON into model objects. By adding model objects, we have improved the code in our view controllers because, instead of reading raw JSON, we're reading properties from a model. And the final improvement that this modification gives us is that our view controllers are a lot cleaner.

One more metric that has improved by refactoring our code is the amount of code that is covered by our tests. We can measure this metric with Xcode's built-in code coverage tracking. We'll look at how to use this tool next.