The Swift Result
type is handy way to capture the results of a throwing expression that you need to execute on a background thread. Use flatMap
to chain several results together.
The Swift Result Type
Apple added the Result
type to the Swift Standard Library in Swift 5. It’s an enum with a success and failure case both of which have an associated value. You can use any type for Success
but Failure
is constrained to Error
:
enum Result<Success, Failure> where Failure : Error {
case success(Success)
case failure(Failure)
}
Using Result To Clean Up Completion Handlers
The Result
type is often used to clean up the completion handlers of asynch API’s like URLSession
. The dataTask(with:completionHandler:)
method has a completion handler with three optional parameters:
let task = session.dataTask(with: url) { data, response, error in
// what combinations of data, response
// or error are expected here?
}
If you’re unfamiliar with this API it can be hard to know that the completion handler is called either with a non-zero error or with data and a response. You can better represent the success and failure cases with Result
:
extension URLSession {
func load(_ url: URL,
completionHandler: @escaping (Result<(URLResponse, Data), Error>)
-> Void) -> URLSessionDataTask {
let task = ...
return task
}
}
Or if we handle the URLResponse
for the caller and pass the completion handler data or an error (see Gist for details):
extension URLSession {
func load(_ url: URL,
completionHandler: @escaping (Result<Data, Error>) -> Void)
-> URLSessionDataTask {
let task = ...
return task
}
}
The caller switches over the result to get the data or error:
let task = URLSession.shared.load(url) { result in
switch result {
case .success(let data): print(data)
case .failure(let error): print(error)
}
}
That’s great but there’s another, easy to miss, use for Result
.
Create A Result From A Throwing Expression
You can create a Result
from a throwing closure:
// Result<[Country], Error>
let result = Result {
try JSONDecoder().decode([Country].self, from: data)
}
The Result
captures the return value of the expression in the .success
value or any thrown error in the .failure
value. You can convert a Result
back to a throwing expression using get()
:
let countries = try result.get() // can throw
A Practical Example
That’s all a bit theoretical, here’s a practical example. I want a method on my CountryStore
class that loads and decodes country data from a file. You might start by creating a throwing method that runs on the main thread:
final class CountryStore: ObservableObject {
@Published var countries = [Country]()
@Published var error: Error?
func load(_ url: URL) throws {
let data = try Data(contentsOf: url)
countries = try PropertyListDecoder().decode([Country].self,
from: data)
}
}
A better way is to make this a non-throwing method that loads and decodes on a background thread:
func load(_ url URL) {
DispatchQueue.global(qos: .background).async { [weak self] in
// Non-throwing block to execute
}
}
Since we can’t throw errors in the block we can wrap the throwing methods with a Result
. First to load the data:
let result = Result { try Data(contentsOf: url) }
This gives us a result
of type Result<Data, Error>
.
Using flatMap
We can chain operations together transforming the result each time with a flatMap
(I never remember that it’s flatMap
and not flatmap
):
.flatMap { data in
Result {
try PropertyListDecoder().decode([Country].self, from: data)
}
}
The flatMap
unwraps the second result for us giving us a final result
of type Result<[Country], Error>
. A plain map
would have given us a nested result
of type Result<Result<[Country],Error>, Error>
.
We then dispatch back to the main thread to update our published properties with the result:
DispatchQueue.main.async {
switch result {
case .success(let countries):
self?.countries = countries
case .failure(let error):
self?.error = error
}
}
We can clean up the unwieldy syntax with an extension on Data
:
extension Data {
static func contentsOf(_ url: URL,
options: Data.ReadingOptions = []) -> Result<Data,Error> {
Result { try Data(contentsOf: url, options: options) }
}
}
Doing the same for PropertyListDecoder
:
extension PropertyListDecoder {
static func decode<T: Decodable>(_ data: Data) -> Result<T,Error> {
Result { try PropertyListDecoder().decode(T.self, from: data) }
}
}
Our two throwing methods to load and decode data then reduce to this:
let result: Result<[Country],Error> = Data.contentsOf(url)
.flatMap(PropertyListDecoder.decode)
The only downside is that we need to help the compiler with the result type. The full method:
final class CountryStore: ObservableObject {
@Published var countries = [Country]()
@Published var error: Error?
func loadStore(_ url: URL) {
typealias CountryResult = Result<[Country], Error>
DispatchQueue.global(qos: .background).async { [weak self] in
let result: CountryResult = Data.contentsOf(url)
.flatMap(PropertyListDecoder.decode)
DispatchQueue.main.async {
switch result {
case .success(let countries):
self?.countries = countries
case .failure(let error):
self?.error = error
}
}
}
}
}
Not exactly a work of art compared to the throwing version but it has more to do. A method to save the store is more compact. The result is of type Result<Void,Error>
so we only need to look at the failure case:
func saveStore(_ url: URL) {
DispatchQueue.global(qos: .background).async { [weak self] in
let result = PropertyListEncoder.encode(self?.countries)
.flatMap { Data.write($0, to: url) }
if case .failure(let error) = result {
DispatchQueue.main.async {
self?.error = error
}
}
}
}
See Swift If Case Let for a reminder on how to use if-case-let
to avoid a full switch
statement when you only care about one case.