Using a local server is a convenient solution during development because it makes it straightforward to set up a development environment. It also allows for flexibility, making it easy to tweak the implementation of the backend as you develop the client.
The drawback is obvious, though. The client application only shows data if it is connected to the local server or if it has permission to connect to the staging or production server. In this episode, we revisit the power of protocol-oriented programming to create a mock server for the development environment.
Using a Mock Server
As the name suggests, a mock server mimics the behavior of a local or remote server. A mock server is often used for testing purposes and that is something we explore later in this series. In this episode, I want to show you how to use a mock server for replacing a local server. This means that you don't need to run an instance of the local server to run the Cocoacasts client. At the end of this episode, you are able to download the source files of the Cocoacasts client and run it in the simulator.
Creating a Protocol
Create a new group in the Networking > APIClient group and name it Protocols. Add a Swift file to the Protocols group and name it APIClient.swift. We define an internal protocol with name APIClient that extends AnyObject. By extending AnyObject, types conforming to the APIClient protocol are required to be classes, that is, reference types.
import Foundation
internal protocol APIClient: AnyObject {
}
Remember that we already have a file with name APIClient.swift and a final class with name APIClient in the Networking > APIClient group. The compiler won't like this. Let's rename the APIClient class. Create a new group with name Clients in the Networking > APIClient group and move APIClient.swift to the Clients group. Rename APIClient.swift to CocoacastsClient.swift and rename the APIClient class to the CocoacastsClient class. Last but not least, we conform the CocoacastsClient class to the APIClient protocol.
import Foundation
import CocoaLumberjack
final class CocoacastsClient: APIClient {
...
}
Before we implement the APIClient protocol, we need to move some of the code of the CocoacastsClient class. Create a group with name Types in the Networking > APIClient group. Add a Swift file with name APIError.swift to the Types group and open CocoacastsClient.swift on the right of APIError.swift. Move the APIClientError enum from CocoacastsClient.swift to APIError.swift and rename it to APIError. Define it as an internal enum.
import Foundation
internal enum APIError: Error {
// MARK: - Cases
case requestFailed
case invalidResponse
}
In the CocoacastsClient class, we need to rename APIClientError to APIError in the fetchEpisodes(_:) method.
// MARK: - Public API
func fetchEpisodes(_ completion: @escaping (Result<[Episode], APIError>) -> Void) {
...
}
Add another Swift file with name APIEndpoint.swift to the Types group and open CocoacastsClient.swift on the right of APIEndpoint.swift. Move the Endpoint enum from CocoacastsClient.swift to APIEndpoint.swift and rename it to APIEndpoint. Define it as an internal enum.
import Foundation
internal enum APIEndpoint: String {
// MARK: - Cases
case episodes
// MARK: - Properties
var path: String {
return rawValue
}
}
In the CocoacastsClient class, we need to rename Endpoint to APIEndpoint in the request(for:) method.
// MARK: - Helper Methods
private func request(for endpoint: APIEndpoint) -> URLRequest {
...
}
Implementing the APIClient Protocol
Open APIClient.swift on the left and CocoacastsClient.swift on the right. Copy the method signature of the fetchEpisodes(_:) method in CocoacastsClient.swift and add it to the APIClient protocol in APIClient.swift.
import Foundation
internal protocol APIClient: AnyObject {
func fetchEpisodes(_ completion: @escaping (Result<[Episode], APIError>) -> Void)
}
Updating the Environment
Open FeedCoordinator.swift and navigate to the feedViewController property. We currently create an APIClient instance by asking the Environment enum for the API key and the base URL.
private lazy var feedViewController: FeedViewController = {
// Initialize API Client
let apiClient = APIClient(apiKey: Environment.apiKey, baseUrl: Environment.baseUrl)
...
}()
This is fine, but I'd like to encapsulate the creation of the APIClient instance in the Environment enum. Why is that? I want to make sure the FeedCoordinator class doesn't know how the APIClient is instantiated. Let me show you what I mean.
Open Environment.swift and declare the apiKey and baseUrl static properties privately. That is the first step.
import Foundation
enum Environment: String {
// MARK: - Environments
case staging
case production
case development
...
// MARK: - Base URL
private static var baseUrl: URL {
...
}
// MARK: - API Key
private static var apiKey: String {
...
}
...
}
The next step is defining a static property, apiClient, of type APIClient. We keep the implementation straightforward for the time being. We initialize an instance of the CocoacastsClient class and return it.
import Foundation
enum Environment: String {
// MARK: - Environments
case staging
case production
case development
...
// MARK: - Base URL
private static var baseUrl: URL {
...
}
// MARK: - API Key
private static var apiKey: String {
...
}
// MARK: - API Client
static var apiClient: APIClient {
return CocoacastsClient(apiKey: apiKey, baseUrl: baseUrl)
}
...
}
Let's revisit the FeedCoordinator class. The feed coordinator no longer creates the APIClient instance. It asks the Environment enum for an APIClient instance instead.
private lazy var feedViewController: FeedViewController = {
// Initialize API Client
let apiClient = Environment.apiClient
...
}()
Creating a Mock Client
It's time to take advantage of the groundwork we laid in this episode. Add a Swift file to the Networking > APIClient > Clients group and name it MockClient.swift. Define a final class with name MockClient that conforms to the APIClient protocol.
/**
The `MockClient` class can be used to locally
test the Cocoacasts application without
the need for a local server.
*/
import Foundation
final class MockClient: APIClient {
}
We implement the fetchEpisodes(_:) method in a moment. Before we continue, we need to add the mock data for the /episodes endpoint, that is, the data the MockClient class returns. Create a new group in the Resources group and name it Mock API. We add the mock data for the /episodes endpoint to the Mock API group and name it episodes.json. You can find the mock data in the finished project of this episode.
Conforming to the APIClient Protocol
The next step is implementing the fetchEpisodes(_:) method in the MockClient class in MockClient.swift.
/**
The `MockClient` class can be used to locally
test the Cocoacasts application without
the need for a local server.
*/
import Foundation
final class MockClient: APIClient {
// MARK: - API Client
func fetchEpisodes(_ completion: @escaping (Result<[Episode], APIError>) -> Void) {
}
}
We make ample use of fatal errors for severals reasons. Loading the mock data and passing it to the completion handler of the fetchEpisodes(_:) method should not fail. Keep in mind that we only use the MockClient class during development.
We ask the main bundle for the URL of episodes.json, the mock data we added a few moments ago. We use a guard statement and throw a fatal error if the MockClient instance isn't able to obtain the URL for episodes.json. This should never happen.
// MARK: - API Client
func fetchEpisodes(_ completion: @escaping (Result<[Episode], APIError>) -> Void) {
// Find Mock Response
guard let url = Bundle.main.url(forResource: "episodes", withExtension: "json") else {
fatalError("Unable to Find Mock Response")
}
}
We use the URL to create a Data object using the try? keyword and a guard statement. We throw a fatal error if the MockClient instance isn't able to load the mock data from the application bundle.
// MARK: - API Client
func fetchEpisodes(_ completion: @escaping (Result<[Episode], APIError>) -> Void) {
// Find Mock Response
guard let url = Bundle.main.url(forResource: "episodes", withExtension: "json") else {
fatalError("Unable to Find Mock Response")
}
// Load Mock Response
guard let data = try? Data(contentsOf: url) else {
fatalError("Unable to Load Mock Response")
}
}
We initialize an instance of the JSONDecoder class to convert the Data object to an array of Episode objects. We set the date decoding strategy of the JSON decoder to iso8601 and the key decoding strategy to convertFromSnakeCase. This isn't new. The JSON decoder used in the CocoacastsClient class is configured identically.
We ask the JSONDecoder instance to decode the Data object. The result should be an array of Episode objects. We use the try? keyword in combination with a guard statement and throw another fatal error if the MockClient instance isn't able to decode the mock data.
// MARK: - API Client
func fetchEpisodes(_ completion: @escaping (Result<[Episode], APIError>) -> Void) {
// Find Mock Response
guard let url = Bundle.main.url(forResource: "episodes", withExtension: "json") else {
fatalError("Unable to Find Mock Response")
}
// Load Mock Response
guard let data = try? Data(contentsOf: url) else {
fatalError("Unable to Load Mock Response")
}
// Initialize JSON Decoder
let decoder = JSONDecoder()
// Configure JSON Decoder
decoder.dateDecodingStrategy = .iso8601
decoder.keyDecodingStrategy = .convertFromSnakeCase
// Decode JSON Response
guard let episodes = try? decoder.decode([Episode].self, from: data) else {
fatalError("Unable to Decode Mock Response")
}
}
The array of episodes is passed to the completion handler of the fetchEpisodes(_:) method.
// MARK: - API Client
func fetchEpisodes(_ completion: @escaping (Result<[Episode], APIError>) -> Void) {
// Find Mock Response
guard let url = Bundle.main.url(forResource: "episodes", withExtension: "json") else {
fatalError("Unable to Find Mock Response")
}
// Load Mock Response
guard let data = try? Data(contentsOf: url) else {
fatalError("Unable to Load Mock Response")
}
// Initialize JSON Decoder
let decoder = JSONDecoder()
// Configure JSON Decoder
decoder.dateDecodingStrategy = .iso8601
decoder.keyDecodingStrategy = .convertFromSnakeCase
// Decode JSON Response
guard let episodes = try? decoder.decode([Episode].self, from: data) else {
fatalError("Unable to Decode Mock Response")
}
// Invoke Handler
completion(.success(episodes))
}
Putting the Mock Client to Use
Open Environment.swift. It's time to put the MockClient class to use. Navigate to the apiClient static property and add a switch statement. If the current environment is the development environment, then we initialize and return an instance of the MockClient class. For the staging and production environment, we initialize and return an instance of the CocoacastsClient class.
// MARK: - API Client
static var apiClient: APIClient {
switch current {
case .development:
return MockClient()
default:
return CocoacastsClient(apiKey: apiKey, baseUrl: baseUrl)
}
}
Select the Cocoacasts/Development scheme at the top. Build and run the application to see the result. The Feed tab should display the mock data we added to the Mock API group.

Adding Flexibility
The mock data is currently being stored in the application bundle, but that is only an example. It is also possible to ask the MockClient class to fetch the mock data from a remote location. We take a look at that approach in the next episode.