In the previous episode, we added the ability to define how the MockClient class responds. This added flexibility makes the MockClient class much more useful. In the next episode, I show you how we can use the MockClient class to unit test the FeedViewModel class.
I'm happy with the implementation of the MockClient class, but there are a few issues we need to address in this episode. The MockClient class blocks the main thread when it fetches the mock data from the remote server. This is easy to fix.
The more important issue I would like to resolve is code duplication. As the project grows, the MockClient class will support more endpoints and that will result in code duplication if we don't optimize the implementation of the MockClient class. Let me show you what I have in mind.
Adding Another Endpoint
Open APIEndpoint.swift and define a case with name episode and an associated value of type Int. This endpoint is used to ask the Cocoacasts API for a single episode. Because the episode case has an associated value, the APIEndpoint enum can no longer have a raw value of type String.
import Foundation
internal enum APIEndpoint {
// MARK: - Cases
case episodes
case episode(Int)
// MARK: - Properties
var path: String {
return rawValue
}
}
This isn't a major issue, but we need to refactor the implementation of the path computed property since the APIEndpoint enum no longer has a rawValue property. We use a switch statement and manually define the path for each case. For the episode case, we use string interpolation to build the path.
import Foundation
internal enum APIEndpoint {
// MARK: - Cases
case episodes
case episode(Int)
// MARK: - Properties
var path: String {
switch self {
case .episodes:
return "episodes"
case .episode(let id):
return "episode/\(id)"
}
}
}
Because the APIEndpoint enum no longer has a raw value of type String, it no longer implicitly conforms to the Hashable protocol. This is a requirement if we want to continue using APIEndpoint objects as keys in the endpoints property of the MockClient class. The solution is simple, though. We explicitly conform the APIEndpoint enum to the Hashable protocol.
import Foundation
internal enum APIEndpoint: Hashable {
// MARK: - Cases
case episodes
case episode(Int)
// MARK: - Properties
var path: String {
switch self {
case .episodes:
return "episodes"
case .episode(let id):
return "episode/\(id)"
}
}
}
The next step is defining a method in the APIClient protocol to fetch an episode. Open APIClient.swift and define a method with name fetchEpisode(with:_:). The method accepts two arguments. The first argument is of type Int and represents the identifier of the episode. The second argument is a completion handler that accepts a single argument of type Result<Episode, APIError>. The associated value of the success case if of type Episode and the associated value of the failure case is of type APIError.
import Foundation
internal protocol APIClient: AnyObject {
func fetchEpisodes(_ completion: @escaping (Result<[Episode], APIError>) -> Void)
func fetchEpisode(with id: Int, _ completion: @escaping (Result<Episode, APIError>) -> Void)
}
Extending the Mock Client
The MockClient class no longer conforms to the APIClient protocol. Let's fix that. The change we need to make is quite simple. We define the fetchEpisode(with:_:) method and copy the body of the fetchEpisodes(_:) method.
func fetchEpisode(with id: Int, _ completion: @escaping (Result<Episode, APIError>) -> Void) {
guard let response = endpoints[.episodes] else {
completion(.failure(.requestFailed))
return
}
switch response {
case .success(let url):
// 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))
case .failure(let error):
completion(.failure(error))
}
}
We need to make a few changes. The endpoint we are interested in is episode. We pass the value of the id parameter as the associated value.
guard let response = endpoints[.episode(id)] else {
completion(.failure(.requestFailed))
return
}
Decoding the Data object should result in an Episode object, not an array of Episode objects. We pass the Episode object as the associated value of success in the completion handler.
// Decode JSON Response
guard let episode = try? decoder.decode(Episode.self, from: data) else {
fatalError("Unable to Decode Mock Response")
}
// Invoke Handler
completion(.success(episode))
The implementations of the fetchEpisodes(_:) and fetchEpisode(with:_:) methods look very similar. We can simplify the implementations using the power and beauty of generics.
Adding Generics to the Mix
We only need to refactor the contents of the success case of the switch statement in the fetchEpisodes(_:) and fetchEpisode(with:_:) methods. The plan is to create a private helper method to load the mock data the URL object points to and convert the resulting Data object into model objects.
We create a private helper method in the MockClient class and name it response(for:_:). The method accepts two arguments. The first argument is the URL object. The second argument is a completion handler. The completion handler accepts a single argument of type T, a generic type. The only constraint the response(for:_:) method enforces on the generic type is that it conforms to the Decodable protocol.
private func response<T: Decodable>(for url: URL, completion: @escaping (T) -> Void) {
}
Remember that loading the mock data blocks the calling thread. This is an issue if the mock data is fetched from a remote server. We work around this issue by performing the work on a background thread. This is easy to do by leveraging Grand Central Dispatch. We ask the DispatchQueue class for a global dispatch queue with the utility quality of service class. We invoke the async(_:) method on the global dispatch queue and pass it the closure that needs to be executed.
private func response<T: Decodable>(for url: URL, completion: @escaping (T) -> Void) {
DispatchQueue.global(qos: .utility).async {
}
}
The next steps are straightforward because we can copy most of the implementation of the fetchEpisodes(_:) and fetchEpisode(with:_:) methods. The response(for:_:) method loads the mock data the URL object points to. It creates a JSONDecoder instance, configures it, and uses it to decode the mock data.
That last step is important. The first argument of the decode(_:from:) method is the type of the value to decode from the mock data. We pass T.self as the first argument and the Data object as the second argument. The result of the decoding operation is passed to the completion handler.
private func response<T: Decodable>(for url: URL, completion: @escaping (T) -> Void) {
DispatchQueue.global(qos: .utility).async {
// Load Mock Data
guard let data = try? Data(contentsOf: url) else {
fatalError("Unable to Load Mock Data")
}
// Initialize JSON Decoder
let decoder = JSONDecoder()
// Configure JSON Decoder
decoder.dateDecodingStrategy = .iso8601
decoder.keyDecodingStrategy = .convertFromSnakeCase
// Decode JSON Response
guard let result = try? decoder.decode(T.self, from: data) else {
fatalError("Unable to Decode Mock Data")
}
completion(result)
}
}
With the response(for:_:) method in place, we can update the fetchEpisodes(_:) and fetchEpisode(with:_:) methods. Let's start with the fetchEpisodes(_:) method. We invoke the response(for:_:) method, passing in the URL object. The completion handler holds the key to the puzzle. We explicitly define the type of the argument that is passed to the completion handler. By specifying the type of the argument, the compiler can infer the concrete type for T in the response(for:_:) method. In the body of the completion handler, the result of the decoding operation is passed to the completion handler of the fetchEpisodes(_:) method.
func fetchEpisodes(_ completion: @escaping (Result<[Episode], APIError>) -> Void) {
guard let response = endpoints[.episodes] else {
completion(.failure(.requestFailed))
return
}
switch response {
case .success(let url):
self.response(for: url) { (episodes: [Episode]) in
completion(.success(episodes))
}
case .failure(let error):
completion(.failure(error))
}
}
The implementation of the fetchEpisode(with:_:) method is almost identical. The only difference is the endpoint and the name and type of the argument of the completion handler of the response(for:_:) method.
func fetchEpisode(with id: Int, _ completion: @escaping (Result<Episode, APIError>) -> Void) {
guard let response = endpoints[.episode(id)] else {
fatalError("No Response Found for Endpoint")
}
switch response {
case .success(let url):
self.response(for: url) { (episode: Episode) in
completion(.success(episode))
}
case .failure(let error):
completion(.failure(error))
}
}
Before we can test the implementation, we need to conform the CocoacastsClient class to the APIClient protocol by adding a placeholder for the fetchEpisode(with:_:) method.
func fetchEpisode(with id: Int, _ completion: @escaping (Result<Episode, APIError>) -> Void) {
}
That's it. Build and run the application. Make sure the Cocoacasts-Development scheme is selected. You should still see a list of episodes in the Feed tab.

What's Next?
With the MockClient class updated, it's time to unit test the FeedViewModel class. That is the focus of the next episode.