Most of the Cocoa APIs we use to build applications are driven by Objective-C. This doesn't mean we need to use Objective-C to take advantage of these APIs, but it does mean that the APIs lack some of the niceties you expect from a Swift API.
Take the URLSession API as an example. Sending a request to a remote API is straightforward using the URLSession API. The implementation is straightforward, but it doesn't feel swifty. Let me show you what I mean.
Using the URLSession API
Fire up Xcode and create a playground by choosing the Blank template from the iOS > Playground section. Name the playground Networking.
Remove the contents of the playground and add an import statement for the UIKit framework.
import UIKit
I would like to fetch an image from a remote server using the URLSession API. We create the URL and, since we are working in a playground, we use the exclamation mark to forced unwrap the result of the initialization.
import UIKit
// Create URL
let url = URL(string: "https://goo.gl/wV9G4I")!
To fetch the data for the image, we create a URLSessionDataTask
instance. We ask the shared URL session for a data task, passing in the URL of the remote resource and a completion handler, a closure. The completion handler is executed when the data task completes, successfully or unsuccessfully.
import UIKit
// Create URL
let url = URL(string: "https://goo.gl/wV9G4I")!
// Create Data Task
let dataTask = URLSession.shared.dataTask(with: url) { (data, _, error) in
}
The completion handler accepts three arguments, an optional Data
object, an optional URLResponse
object, and an optional Error
object. For this example, we are interested in the Data
object and the Error
object. To create a UIImage
instance, we safely unwrap the Data
object and use it to create a UIImage
instance.
import UIKit
// Create URL
let url = URL(string: "https://goo.gl/wV9G4I")!
// Create Data Task
let dataTask = URLSession.shared.dataTask(with: url) { (data, _, error) in
if let data = data, let image = UIImage(data: data) {
print(image)
} else {
}
}
This doesn't look too bad, but we still need to handle the error in the completion handler. That is where the implementation becomes awkward. The error that is passed to the completion handler is of an optional type. In theory, it is possible that the Data
object and the Error
object are both equal to nil
. If we want to play it safe, we need to safely unwrap the Error
object in the else
clause of the completion handler.
import UIKit
// Create URL
let url = URL(string: "https://goo.gl/wV9G4I")!
// Create Data Task
let dataTask = URLSession.shared.dataTask(with: url) { (data, _, error) in
if let data = data, let image = UIImage(data: data) {
print(image)
} else {
if let error = error {
print(error)
} else {
print("no data, no error")
}
}
}
If the Data
object and the Error
object would both be equal to nil
, we would have to throw an unknown error to inform the application that the image could not be fetched from the remote server for an unknown reason.
According to the documentation, this should never happen. The documentation states that the Error
object is equal to nil
if the request is successful. If the request is unsuccessful, the Data
object is equal to nil
and the Error
object is not equal to nil
.
With the documentation in mind, we can update the implementation of the completion handler by using an else if
clause to safely unwrap the Error
object. This may look fine, but we still have a problem. We introduced a code smell. If an if
statement includes one or more else if
clauses, the last else if
clause should be followed by an else
clause. This is similar to the default
clause in a switch
statement. You want to program defensively by making sure you catch any scenarios you didn't catch in the if
or else if
clauses.
import UIKit
// Create URL
let url = URL(string: "https://goo.gl/wV9G4I")!
// Create Data Task
let dataTask = URLSession.shared.dataTask(with: url) { (data, _, error) in
if let data = data, let image = UIImage(data: data) {
print(image)
} else if let error = error {
print(error)
}
}
And that takes us back to square one.
import UIKit
// Create URL
let url = URL(string: "https://goo.gl/wV9G4I")!
// Create Data Task
let dataTask = URLSession.shared.dataTask(with: url) { (data, _, error) in
if let data = data, let image = UIImage(data: data) {
print(image)
} else if let error = error {
print(error)
} else {
print("unknown error")
}
}
Introducing the Result Type
Soon after the introduction of the Swift programming language, developers came up with a solution for this pesky problem. I also introduced a solution in Building a Weather Application From Scratch. Let's take a look at the implementation of the RootViewModel
class. In the fetchWeatherData(for:)
method, the root view model uses a URLSessionDataTask
instance to fetch weather data from the Dark Sky API. In the completion handler, we safely unwrap the Error
object and the Data
object. If both are equal to nil
the else
clause is executed.
private func fetchWeatherData(for location: Location) {
// Initialize Weather Request
let weatherRequest = WeatherRequest(baseUrl: WeatherService.authenticatedBaseUrl, location: location)
// Create Data Task
URLSession.shared.dataTask(with: weatherRequest.url) { [weak self] (data, response, error) in
if let response = response as? HTTPURLResponse {
print("Status Code: \(response.statusCode)")
}
DispatchQueue.main.async {
if let error = error {
print("Unable to Fetch Weather Data \(error)")
// Weather Data Result
let result: WeatherDataResult = .failure(.noWeatherDataAvailable)
// Invoke Completion Handler
self?.didFetchWeatherData?(result)
} else if let data = data {
// Initialize JSON Decoder
let decoder = JSONDecoder()
// Configure JSON Decoder
decoder.dateDecodingStrategy = .secondsSince1970
do {
// Decode JSON Response
let darkSkyResponse = try decoder.decode(DarkSkyResponse.self, from: data)
// Weather Data Result
let result: WeatherDataResult = .success(darkSkyResponse)
// Update User Defaults
UserDefaults.didFetchWeatherData = Date()
// Invoke Completion Handler
self?.didFetchWeatherData?(result)
} catch {
print("Unable to Decode JSON Response \(error)")
// Weather Data Result
let result: WeatherDataResult = .failure(.noWeatherDataAvailable)
// Invoke Completion Handler
self?.didFetchWeatherData?(result)
}
} else {
// Weather Data Result
let result: WeatherDataResult = .failure(.noWeatherDataAvailable)
// Invoke Completion Handler
self?.didFetchWeatherData?(result)
}
}
}.resume()
}
What is interesting about the implementation is the use of the WeatherDataResult
type. Let me show you what it looks like. The WeatherDataResult
enum defines two cases, success
and failure
. The success
case defines an associated value of type WeatherData
. The failure
case defines an associated value of type WeatherDataError
.
import UIKit
class RootViewModel: NSObject {
// MARK: - Types
enum WeatherDataResult {
case success(WeatherData)
case failure(WeatherDataError)
}
enum WeatherDataError: Error {
case notAuthorizedToRequestLocation
case failedToRequestLocation
case noWeatherDataAvailable
}
...
}
This is the solution we have been looking for and it is a solution that isn't available in Objective-C. The WeatherDataResult
enum clearly communicates the outcome of the weather data request, it is successful or unsuccessful. If it is successful, then it has an associated value of type WeatherData
. If it is unsuccessful, then it has an associated value of type WeatherDataError
. There is no third scenario.
Introducing Swift's Native Result Type
I created Building a Weather Application From Scratch before the introduction of Swift 5. That is why the RootViewModel
class defines the WeatherDataResult
type. Swift 5 has made everything much easier and consistent by introducing the Result
type. Let's refactor the RootViewModel
class by replacing the WeatherDataResult
type with Swift's native Result
type.
Let's start by taking a look at Swift's Result
type. It defines two cases, success
and failure
. It also defines two generic types, Success
and Failure
with Failure
required to inherit the Error
protocol. This is a very clever solution. The success
case of the Result
enum has an associated value of type Success
and the failure
case of the Result
enum has an associated value of type Failure
.
/// A value that represents either a success or a failure, including an
/// associated value in each case.
public enum Result<Success, Failure> where Failure : Error {
/// A success, storing a `Success` value.
case success(Success)
/// A failure, storing a `Failure` value.
case failure(Failure)
...
}
With this in mind, we can put the Result
type to use in the RootViewModel
class. The solution is simpler than you might think thanks to the WeatherDataResult
enum. We define a type alias with name WeatherDataResult
. The type of the type alias is Result<WeatherData, WeatherDataError>
.
import UIKit
class RootViewModel: NSObject {
// MARK: - Type Aliases
typealias WeatherDataResult = Result<WeatherData, WeatherDataError>
// MARK: - Types
enum WeatherDataResult {
case success(WeatherData)
case failure(WeatherDataError)
}
enum WeatherDataError: Error {
case notAuthorizedToRequestLocation
case failedToRequestLocation
case noWeatherDataAvailable
}
...
}
The only other change we need to make is remove the WeatherDataResult
enum.
import UIKit
class RootViewModel: NSObject {
// MARK: - Type Aliases
typealias WeatherDataResult = Result<WeatherData, WeatherDataError>
// MARK: - Types
enum WeatherDataError: Error {
case notAuthorizedToRequestLocation
case failedToRequestLocation
case noWeatherDataAvailable
}
...
}
That's it. We have successfully replaced the WeatherDataResult
enum with Swift's Result
type. By using a type alias, we don't have to replace every occurrence of WeatherDataResult
with Result<WeatherData, WeatherDataError>
. Using a type alias is optional, but it improves the readability.
What's Next?
The addition of Swift's native Result
enum is most welcome because it eliminates the need to define a custom Result
enum. What makes this solution powerful is the combination of enums, generics, and associated values. This combination isn't available in Objective-C and that is why some Objective-C APIs don't feel as elegant as native Swift APIs.