Building Simple Async API Request With Swift 5 Result Type
Published at Mar 29, 2019
Swift 5 has finally been released by Apple to the stable channel at the end of March 2019. It’s bundled in Xcode 10.2 for macOS. Swift 5 provided ABI stability to the Swift Standard Library in Apple platform. This means, all the future Swift version will be binary compatible with the apps written in Swift 5 code. It also introduces App Thinning, shrinking the app size as the ABI stability means the binary doesn’t have to embed all the Swift Standard Library inside the app bundle thus reducing the app size. The operating system will include the Swift runtime and standard library, beginning with iOS 12.2.
You can read all the information related to the future of Swift regarding Module stability and Library evolution in the link at ABI Stability and More.
In this article, we’ll talk about the new Result type for Swift 5 and how we can utilize that to create an asynchronous API request and simplify handling the completion handler closure.
Before we begin, let’s see how we usually create an asynchronous function in Swift. There are several approaches that we usually use.
1. Objective-C Style
Using Objective-C style, we create a single callback closure with several parameters containing the optional result value and the optional error in the asynchronous function.
func fetchMovies(url: URL, completionHandler: @escaping ([Movie]?, Error?) -> Void) {
...
}
2. Swift Style
In this style, we create 2 completion closures. One is for handling success with the result value as the parameter, and one is for handling failure with the error as the parameter.
func fetchMovies(url: URL, successHandler: @escaping ([Movie]) -> Void, errorHandler: @escaping (Error?) -> Void) {
...
}
Introducing Swift 5 Result Type
Swift 5 finally introduces new Result type to handle the result of an asynchronous function using an enum. There are only 2 cases that both uses Swift Generic with associated value:
- Success with the value of the result.
- Failure with the type that implements Error protocol.
The Result type really helps us to simplify handling the result of an asynchronous function as there are only 2 cases to handle, success and failure. Let’s begin by creating a simple extension for the URLSession data task that use the Result type for the completion handler closure.
Extending the URL Session Data Task with Result Type
As we all know when making a data task using URLSession, we need to pass the completion handler closure with the parameters of URLResponse, Data, and Error. Their type all are optionals, which we need to check using if let or guard statement to check the http response status code, retrieve the data, and decode the data into the model.
let url = URL(string: "https://exampleapi.com/data.json")!URLSession.shared.dataTask(with: url) { (data: Data?, response: URLResponse?, error: Error?) in
if let error = error {
// Handle Error
return
} guard let response = response else {
// Handle Empty Response
return
} guard let data = data else {
// Handle Empty Data
return
} // Handle Decode Data into Model
}
This approach is really cumbersome to handle every time. We can create a better solution using the Result Type. So, let’s make a simple extension for the URLSession to handle data task with Result type.
extension URLSession { func dataTask(with url: URL, result: @escaping (Result<(URLResponse, Data), Error>) -> Void) -> URLSessionDataTask {
return dataTask(with: url) { (data, response, error) in
if let error = error {
result(.failure(error))
return
} guard let response = response, let data = data else {
let error = NSError(domain: "error", code: 0, userInfo: nil)
result(.failure(error))
return
}
result(.success((response, data)))
}
}
}
We can call this function like we usually invoke the dataTask passing the URL, instead this time we will pass the completion closure with Result Type as the parameter.
URLSession.shared.dataTask(with: url) { (result) in
switch result {
case .success(let response, let data):
// Handle Data and Response
break case .failure(let error):
// Handle Error
break
}
}
This approach looks pretty simple and clean compared to using previous completion handle without Result type.
Create API Service for The MovieDB (TMDb) API
Let’s use our new knowledge to build a simple MovieServiceAPI class that used The Movie Database (TMDb) API to retrieve movies by category, single movie by Id. You can register for the API key in the link at API Docs TMDb
First let’s create the Model that implements the Codable to handle decoding the JSON data into the Swift Model.
public struct MoviesResponse: Codable {
public let page: Int
public let totalResults: Int
public let totalPages: Int
public let results: [Movie]
}public struct Movie: Codable {
public let id: Int
public let title: String
public let overview: String
public let releaseDate: Date
public let voteAverage: Double
public let voteCount: Int
public let adult: Bool
}
Next, let’s create the class with all the properties such as the baseURL, apiKey, jsonDecoder and endpoint that is being represented by enum.
class MovieServiceAPI {
public static let shared = MovieServiceAPI() private init() {}
private let urlSession = URLSession.shared
private let baseURL = URL(string: "https://api.themoviedb.org/3")!
private let apiKey = "PUT_API KEY HERE" private let jsonDecoder: JSONDecoder = {
let jsonDecoder = JSONDecoder()
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-mm-dd"
jsonDecoder.dateDecodingStrategy = .formatted(dateFormatter)
return jsonDecoder
}() // Enum Endpoint
enum Endpoint: String, CustomStringConvertible, CaseIterable {
case nowPlaying = "now_playing"
case upcoming
case popular
case topRated = "top_rated"
}
}
We also need to create an Error enum to represent several error case in our API request.
class MovieServiceAPI {
...
public enum APIServiceError: Error {
case apiError
case invalidEndpoint
case invalidResponse
case noData
case decodeError
}
}
Next, let’s create a private function that fetch resource using Swift Generic Constraint withdecodable type. This function will accept the URL that will be used to initiate the URLsession data task with Result Type.
private func fetchResources<T: Decodable>(url: URL, completion: @escaping (Result<T, APIServiceError>) -> Void) { guard var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
completion(.failure(.invalidEndpoint))
return
} let queryItems = [URLQueryItem(name: "api_key", value: apiKey)]
urlComponents.queryItems = queryItems guard let url = urlComponents.url else {
completion(.failure(.invalidEndpoint))
return
}
urlSession.dataTask(with: url) { (result) in
switch result {
case .success(let (response, data)):
guard let statusCode = (response as? HTTPURLResponse)?.statusCode, 200..<299 ~= statusCode else {
completion(.failure(.invalidResponse))
return
}
do {
let values = try self.jsonDecoder.decode(T.self, from: data)
completion(.success(values))
} catch {
completion(.failure(.decodeError))
}
case .failure(let error):
completion(.failure(.apiError))
}
}.resume()
}
In the URLSession dataTask with completion closure, we check the result enum. If success, we retrieve the data then check the response HTTP status code to make sure it’s in the range between 200 and 299. Then, using jsonDecoder we decode the json data with the generic decodable constraint of the model from the function parameter.
Fetch Movie List from Endpoint
Next, let’s create our first method to retrieve list of movies using specific endpoint. First, we start defining the URL by appending the baseURL with the endpoint passed. Then, let’s use the fetchResource method that we have created before to retrieve the movies array that is already decoded.
class MovieServiceAPI {
...
public func fetchMovies(from endpoint: Endpoint, result: @escaping (Result<MoviesResponse, APIServiceError>) -> Void) { let movieURL = baseURL
.appendingPathComponent("movie")
.appendingPathComponent(endpoint.rawValue)
fetchResources(url: movieURL, completion: result)
}
To use the method, we can call it just like this:
MovieAPIService.shared.fetchMovies(from: .nowPlaying) { (result: Result<MoviesResponse, MovieAPIService.APIServiceError>) in switch result {
case .success(let movieResponse):
print(movieResponse.results) case .failure(let error):
print(error.localizedDescription)
}
}
Fetch Single Movie with Movie Id
For retrieving single movie using a movie id, we’ll construct the url by appending movie id, then using the same fetchResource method we retrieve a single decoded movie for the result.
class MovieServiceAPI {...
public func fetchMovie(movieId: Int, result: @escaping (Result<Movie, APIServiceError>) -> Void) {
let movieURL = baseURL
.appendingPathComponent("movie")
.appendingPathComponent(String(movieId)) fetchResources(url: movieURL, completion: result)
}
To use the method, we can call it just like this:
MovieAPIService.shared.fetchMovie(movieId: 1234) { (result: Result<Movie, MovieAPIService.APIServiceError>) in
switch result {
case .success(let movie):
print(movie)
case .failure(let error):
print(error.localizedDescription)
}
}
Conclusion
That’s it, our API service looks pretty simple and clean with Result Type as the completion handler. There are many other features for Result Type such as map and flatMap Result to another Result. Result type really simplify the callback completion handler into success and failure case.
Swift 5 is super amazing and i believe there are many more amazing new features that we can look forward for the next evolution of Swift. Keep the lifelong learning goes on and happy Swifting 😋.