The playground has been useful to create a prototype of the APIClient class. It's time to integrate the Episode struct and the APIClient class into the project. That is the focus of this episode.

Adding the Episode and APIClient Types

Before we integrate the APIClient class into the project, we add the Episode struct to the project. We create a group with name Models and add a Swift file with name Episode.swift to the Models group. We copy the implementation of the playground to Episode.swift. We remove the public keyword because the Episode struct doesn't need to be defined publicly in the project.

import Foundation

struct Episode: Decodable {

    // MARK: - Properties

    let id: Int
    let plus: Bool
    let title: String
    let excerpt: String
    let author: String
    let thumbnailUrl: URL
    let collection: String?
    let publishedAt: Date

    let swift: Int
    let xcode: Int
    let platformName: String
    let platformVersion: Int

}

We create another group with name Networking and add a subgroup with name API Client to the Networking group. We add a Swift file to the API Client group and name it APIClient.swift. We copy the implementation of the playground to APIClient.swift and remove the public keywords. The APIClient class, its methods, and the types it defines don't need to be defined publicly in the project.

import Foundation

final class APIClient {

    // MARK: - Types

    enum APIClientError: Error {
        case requestFailed
        case invalidResponse
    }

    // MARK: -

    private enum Endpoint: String {

        // MARK: - Cases

        case episodes

        // MARK: - Properties

        var path: String {
            return rawValue
        }

    }

    // MARK: - Properties

    private let apiKey: String

    // MARK: -

    private let baseUrl: URL

    // MARK: - Initialization

    init(apiKey: String, baseUrl: URL) {
        // Set Properties
        self.apiKey = apiKey
        self.baseUrl = baseUrl
    }

    // MARK: - Public API

    func fetchEpisodes(_ completion: @escaping (Result<[Episode], APIClientError>) -> Void) {
        ...
    }

    // MARK: - Helper Methods

    private func request(for endpoint: Endpoint) -> URLRequest {
        ...
    }

}

Before we continue, we add an import statement for the CocoaLumberjack library at the top and replace the print statements in the fetchEpisodes(_:) method with DDLogError statements.

import Foundation
import CocoaLumberjack

final class APIClient {
    ...
}
// MARK: - Public API

func fetchEpisodes(_ completion: @escaping (Result<[Episode], APIClientError>) -> Void) {
    // Initialize and Initiate Data Task
    URLSession.shared.dataTask(with: request(for: .episodes)) { (data, response, error) in
        if let data = data {
            do {
                // Initialize JSON Decoder
                let decoder = JSONDecoder()

                // Configure JSON Decoder
                decoder.dateDecodingStrategy = .iso8601
                decoder.keyDecodingStrategy = .convertFromSnakeCase

                // Decode JSON Response
                let episodes = try decoder.decode([Episode].self, from: data)

                // Invoke Handler
                completion(.success(episodes))
            } catch {
                // Invoke Handler
                completion(.failure(.invalidResponse))
            }

        } else {
            // Invoke Handler
            completion(.failure(.requestFailed))

            if let error = error {
                DDLogError("Unable to Fetch Episodes \(error)")
            } else {
                DDLogError("Unable to Fetch Episodes")
            }
        }
    }.resume()
}

Model-View-ViewModel-Coordinator

The next step is deciding which object instantiates the APIClient instance and which object owns the APIClient instance. These questions are easy to answer since the project adopts the Model-View-ViewModel-Coordinator pattern.

The feed view controller needs a view model and the coordinator that instantiates the feed view controller is responsible for injecting the view model into the feed view controller. This may sound complex if you're not familiar with the MVVM-C pattern. The implementation is straightforward, though. Let's break it down into digestible steps.

Creating a View Model

We create a group with name View Models in the Modules > Feed group. We add a Swift file to the View Models group and name it FeedViewModel.swift. We define a class with name FeedViewModel and prefix the class definition with the final keyword.

import Foundation

final class FeedViewModel {

}

The FeedViewModel class defines a private, constant property, apiClient, of type APIClient. The APIClient instance is injected into the feed view model during initialization. We could put the FeedViewModel class in charge of creating the APIClient instance, but I prefer to inject dependencies whenever possible. It makes unit testing much easier.

import Foundation

final class FeedViewModel {

    // MARK: - Properties

    private let apiClient: APIClient

}

This means that the next step is defining an initializer that accepts an APIClient instance. The feed view model keeps a reference to the APIClient instance in its apiClient property.

import Foundation

final class FeedViewModel {

    // MARK: - Properties

    private let apiClient: APIClient

    // MARK: - Initialization

    init(apiClient: APIClient) {
        // Set Properties
        self.apiClient = apiClient
    }

}

Injecting the View Model

Open FeedViewController.swift and define a variable property, viewModel, of type FeedViewModel?. Because we use storyboards, we have no other option but to define the viewModel property as a variable property of type FeedViewModel?. We could define the viewModel property as an implicitly unwrapped optional, but that's a pattern I tend to avoid.

import UIKit

class FeedViewController: UIViewController, Storyboardable {

    // MARK: - Storyboardable

    static var storyboardName: String {
        return "Feed"
    }

    // MARK: - Properties

    var viewModel: FeedViewModel?

    // MARK: - Initialization

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        // Set Title
        title = "Feed"
    }

}

It's time to create and inject the view model into the FeedViewController instance. Let's revisit the FeedCoordinator class in FeedCoordinator.swift. We no longer define the feedViewController property as a constant. We define feedViewController as a lazy, variable property instead.

import UIKit

class FeedCoordinator: Coordinator {

    // MARK: - Properties

    var rootViewController: UIViewController {
        return feedViewController
    }

    // MARK: -

    private lazy var feedViewController: FeedViewController = {

    }()

}

We need to initialize several objects. The initializer of the FeedViewModel class requires an APIClient instance. The APIClient class requires an API key and a base URL. We start by creating the base URL. This should look familiar. Don't worry about the exclamation mark. We resolve this red flag later in this episode.

private lazy var feedViewController: FeedViewController = {
    // Initialize Base URL
    let baseUrl = URL(string: "http://0.0.0.0:3000/api/")!
}()

The base URL is used to create the APIClient instance. The API key is passed to the initializer as a string literal. I don't like random string literals floating around in a project. We clean the implementation up later in this episode.

private lazy var feedViewController: FeedViewController = {
    // Initialize Base URL
    let baseUrl = URL(string: "http://0.0.0.0:3000/api/")!

    // Initialize API Client
    let apiClient = APIClient(apiKey: "9e8d3f9fd2ce713bb1ca8e60021e09d0dc6fb654", baseUrl: baseUrl)
}()

We pass the APIClient instance to the initializer of the FeedViewModel class.

private lazy var feedViewController: FeedViewController = {
    // Initialize Base URL
    let baseUrl = URL(string: "http://0.0.0.0:3000/api/")!

    // Initialize API Client
    let apiClient = APIClient(apiKey: "9e8d3f9fd2ce713bb1ca8e60021e09d0dc6fb654", baseUrl: baseUrl)

    // Initialize Feed View Model
    let viewModel = FeedViewModel(apiClient: apiClient)
}()

We invoke the instantiate() method on the FeedViewController class to create the feed view controller and set its viewModel property. We return the FeedViewController instance from the closure. Don't forget to append the trailing pair of parentheses to execute the closure.

private lazy var feedViewController: FeedViewController = {
    // Create Base URL
    let baseUrl = URL(string: "http://0.0.0.0:3000/api/")!

    // Initialize API Client
    let apiClient = APIClient(apiKey: "9e8d3f9fd2ce713bb1ca8e60021e09d0dc6fb654", baseUrl: baseUrl)

    // Initialize Feed View Model
    let viewModel = FeedViewModel(apiClient: apiClient)

    // Initialize Feed View Controller
    let feedViewController = FeedViewController.instantiate()

    // Configure Feed View Controller
    feedViewController.viewModel = viewModel

    return feedViewController
}()

Let's test the implementation by invoking the fetchEpisodes(_:) method in the initializer of the FeedViewModel class. We print the number of episodes if the API request is successful and we print the error if anything goes wrong.

import Foundation

final class FeedViewModel {

    // MARK: - Properties

    private let apiClient: APIClient

    // MARK: - Initialization

    init(apiClient: APIClient) {
        // Set Properties
        self.apiClient = apiClient

        // Fetch Episodes
        apiClient.fetchEpisodes { (result) in
            switch result {
            case .success(let episodes):
                print(episodes.count)
            case .failure(let error):
                print(error)
            }
        }
    }

}

Let's build and run the application in the simulator and inspect the output in the console. The output confirms that we successfully integrated the APIClient class into the project.

20

Improving the Implementation

If I want to switch from the development environment to staging or production, I need to modify the API key and the base URL in FeedCoordinator.swift. That is far from ideal. There is a much better solution and we laid the foundation earlier in this series. Let's open Environment.swift in the Configuration group. This is what the implementation currently looks like.

import Foundation

enum Environment: String {

    // MARK: - Environments

    case staging
    case production

    // MARK: - Current Environment

    static let current: Environment = {
        // Read Value From Info.plist
        guard let value = Bundle.main.infoDictionary?["CONFIGURATION"] as? String else {
            fatalError("No Configuration Found")
        }

        // Extract Environment
        guard let rawValue = value.split(separator: "/").last else {
            fatalError("Invalid Environment")
        }

        // Create Environment
        guard let environment = Environment(rawValue: rawValue.lowercased()) else {
            fatalError("Invalid Environment")
        }

        return environment
    }()

    // MARK: - Base URL

    static var baseUrl: URL {
        switch current {
        case .staging:
            return URL(string: "https://staging.cocoacasts.com/api/")!
        case .production:
            return URL(string: "https://cocoacasts.com/api/")!
        }
    }

}

We can already ask an Environment object for its base URL. We need to make a few small changes. First, the Environment enum needs to support the development environment. Second, an Environment object should be able to return the API key for the current environment.

Adding the Development Environment

We start by adding a case with name development to the Environment enum.

import Foundation

enum Environment: String {

    // MARK: - Environments

    case staging
    case production
    case development

    ...
}

This means that we also need to update the implementation of the baseUrl static property. We add a case for the development environment and return the base URL of the development environment.

// MARK: - Base URL

static var baseUrl: URL {
    switch current {
    case .staging:
        return URL(string: "https://staging.cocoacasts.com/api/")!
    case .production:
        return URL(string: "https://cocoacasts.com/api/")!
    case .development:
        return URL(string: "http://0.0.0.0:3000/api/")!
    }
}

The next step is adding a static property for the API key. We define a static, variable property, apiKey, of type String. We return a different API key for each environment.

// MARK: - API Key

static var apiKey: String {
    switch current {
    case .staging:
        return "1bb7a1a05b30ac1ae897258f15b0a1c63fa62ea7"
    case .production:
        return "08b3b2b574dfe3571205678453ae33599e79bf6a"
    case .development:
        return "9e8d3f9fd2ce713bb1ca8e60021e09d0dc6fb654"
    }
}

With the implementation of the Environment enum updated, it's time to revisit the FeedCoordinator class. The feed coordinator can now ask the Environment enum for the API key and the base URL for the current environment. This looks much better.

private lazy var feedViewController: FeedViewController = {
    // Initialize API Client
    let apiClient = APIClient(apiKey: Environment.apiKey, baseUrl: Environment.baseUrl)

    // Initialize Feed View Model
    let viewModel = FeedViewModel(apiClient: apiClient)

    // Initialize Feed View Controller
    let feedViewController = FeedViewController.instantiate()

    // Configure Feed View Controller
    feedViewController.viewModel = viewModel

    return feedViewController
}()

Switching Environments

It's important to make switching environments as painless as possible. You don't want to update a build setting or a configuration file every time you switch environments. Remember from earlier in this series that switching environments is as simple as selecting a scheme. We have the Cocoacasts/Staging scheme and the Cocoacasts/Production scheme. We only need to add a configuration for the development environment and create a scheme.

Click the project in the Project Navigator and select the Cocoacasts project on the left. Because we don't plan to create a release build for the development environment, we only need to add one configuration. Click the + button below the list of configurations and choose Duplicate "Debug/Staging" Configuration. Name the new configuration Debug/Development.

Adding a Configuration

Click the active scheme in the top left and choose Manage Schemes.... Select the Cocoacasts/Staging scheme, click the gear icon at the bottom, and choose Duplicate.

Adding a Scheme

Name the new scheme Cocoacasts/Development and set Build Configuration to Debug/Development. Click the Close button in the bottom right to commit the changes.

Configuring a Scheme

Make sure the Cocoacasts/Development scheme is selected. Build and run the application in the simulator and inspect the output in the console. You should still see the number of episodes the API client fetched from the local Cocoacasts API.

20

We also need to update the project's Podfile. Close the workspace and open the project's Podfile in a text editor. The Reveal SDK needs to be installed for the Debug/Staging, Debug/Production, and Debug/Development configurations.

target 'Cocoacasts' do
  platform :ios, '12.0'
  use_frameworks!
  inhibit_all_warnings!
  ...

  # Development
  pod 'Reveal-SDK', configurations: ['Debug/Staging', 'Debug/Production', 'Debug/Development']

  ...
end

Make sure to run bundle exec pod install from the command line to update the Pods project.

What's Next?

Integrating the APIClient class into the project wasn't difficult and that is in part thanks to the Model-View-ViewModel-Coordinator pattern. The MVVM-C pattern clearly defines the responsibilities in the project.

The view controller should only be concerned with managing its view and responding to user interaction. The view controller's view model owns the APIClient instance and the coordinator that instantiates the view controller creates the view model and injects it into the view controller.

By injecting the APIClient instance into the view model, we lay the foundation for a robust test suite. We start writing unit tests later in this series.