Exploring Freelancing
Navigate freelancing as a developer; find clients, manage contracts, ensure timely payment, and learn from experiences!
Last year, I created QuoteKit, a Swift framework to use the free APIs provided by Quotable. It uses the latest async/await syntax for easy access and contains all the APIs like fetching a random quote, all quotes, authors, tags, and searching quotes and authors.
At that time, this new syntax was available only for iOS 15+, so to support iOS 13 and above, I added alternatives that used completion handlers.
It uses a struct QuotableEndpoint
for defining the endpoint URL. Then, the main struct has two generic methods for executing requests, one for the normal completion handler and the other using the new async syntax.
Using Completion Handlers
The first method is what you’re probably familiar with:
public struct QuoteKit {
static func execute<Model: Decodable>(with endpoint: QuotableEndpoint, completion: @escaping (Result<Model, Error>) -> ()) {
let task = URLSession.shared.dataTask(with: endpoint.url) { data, _, error in
if let error = error {
completion(.failure(error))
return
}
guard let response = response else {
completion(.failure(URLError(.badServerResponse))
return
}
guard let data = data else {
completion(.failure(QuoteFetchError.missingData))
return
}
do {
let model = try JSONDecoder().decode(Model.self, from: data)
completion(.success(model))
} catch let decodingError {
completion(.failure(decodingError))
}
}
task.resume()
}
}
It takes a Model
type that conforms to Decodable and returns the Result
enum containing the Model
for the success value.
Then, it calls the dataTask(with:completionHandler:)
method on the URLSession
shared instance. The handler returns the data, response, and error. If there’s an error, we pass that as the failure of the Result
enum. Similarly, if the data is nil,
it gives a custom missingData
error. And finally, while decoding, if there’s a decoding error, it throws that error.
Using it in one of the methods in QuoteKit that fetches the quote for a particular ID:
public extension QuoteKit {
static func quote(id: String, completion: @escaping (Result<Quote?, Error>) -> ()) {
execute(with: QuotableEndpoint(.quote(id)), completion: completion)
}
}
Now, you use this completion handler in your project like:
func fetchParticularQuote(for id: String) async {
QuoteKit.quote(id: id) { result in
switch result {
case .success(let quote):
print(quote)
case .failure(let error):
print(error.localizedDescription)
}
}
}
Although this is a nice way of working with completion handlers, I never appreciated it. I was always wished if there was a way to write an asynchronous network request similar to how you write your normal functions.
And in Swift 5.5, we got concurrency in Swift! After the first initial months, we also got support for iOS 13 and above!
I can safely remove the methods that use completion handlers as the package is iOS 13+. If there are requests, I’ll add them back, but in the future, I only want to support the async/await syntax for writing new APIs.
Now, where were we? Time for the new syntax!
Using async/await Syntax
Creating an async method that throws is pretty simple. The method uses the same Model, but instead of a completion handler that returns a result, now you use the keyword async
and normally return the Model
with a similar syntax of a synchronous method. Also, you throw any error for the client-side to handle.
You call the data(from:)
method on the URLSession
shared instance that returns a tuple of data and response. You can either handle the response or (not recommended) ignore it. Note how seamless this one line of code looks as if you’re calling a normal synchronous method!
Then, you decode the data and return the Model
, and throw any error that may happen. And that’s it!
public struct QuoteKit {
static func execute<Model: Decodable>(with endpoint: QuotableEndpoint) async throws -> Model {
let (data, _) = try await URLSession.shared.data(from: endpoint.url)
return try JSONDecoder().decode(Model.self, from: data)
}
}
The problem with this approach is that even though Swift made the syntax backward compatible for previous versions, the URLSession.data(from:)
hasn’t been updated. So, even after getting the above code, you’ll get errors for iOS 13 and iOS 14.
You’ll have to write your implementation and use the completion handlers under the hood.
Bummer, I know.
I did my implementation first, but recently John shared an amazing AsyncCompatibilityKit that adds iOS 13-compatible backport of commonly used async/await-based system APIs that are only available from iOS 15 by default.
I modified it a little to match my earlier implementation:
// Taken from Swift by Sundell -
// (Making async system APIs backward compatible)[https://www.swiftbysundell.com/articles/making-async-system-apis-backward-compatible/]
// Modified implementation from (AsyncCompatibilityKit)[https://github.com/JohnSundell/AsyncCompatibilityKit/blob/main/Sources/URLSession%2BAsync.swift]
@available(iOS, deprecated: 15.0, message: "Use the built-in API instead")
public extension URLSession {
func data(from url: URL) async throws -> (Data, URLResponse) {
var task: URLSessionDataTask?
let onCancel = { task?.cancel() }
return try await withTaskCancellationHandler(handler: { onCancel() }) {
try await withCheckedThrowingContinuation { continuation in
task = self.dataTask(with: url) { data, response, error in
if let error = error {
return continuation.resume(throwing: error)
}
guard let response = response else {
return continuation.resume(throwing: URLError(.badServerResponse))
}
guard let data = data else {
return continuation.resume(throwing: QuoteFetchError.missingData)
}
continuation.resume(returning: (data, response))
}
task?.resume()
}
}
}
}
I still don’t understand how
withTaskCancellationHandler
works, but I’ll subsequently look into it and update it here.
Otherwise, withCheckedThrowingContinuation(function:_:)
helps you wrap the completion handler in an async function, throw errors and return the data and the response as a tuple by calling resume(throwing:)
or resume(returning:)
respectively.
Now, when you use the new method, you’ll see it highlighted by green color instead of blue (if you’re using Xcode’s Midnight theme), indicating that it is calling our implementation instead of system implementation. Using it in one of the methods:
public extension QuoteKit {
static func quote(id: String) async throws -> Quote {
try await execute(with: QuotableEndpoint(.quote(id)))
}
}
You have to prepend the await
keyword while calling the method to signify that it is awaiting the result. In my perspective, this syntax is cleaner and similar to the synchronous code.
I would have loved this syntax while learning iOS development, where I spent a lot of time with completion handlers and was doomed to deal with the pyramid of doom.
Using it in the app:
func fetchParticularQuote(for id: String) async {
do {
let quote = try await QuoteKit.quote(id: "1234")
print(quote)
} catch {
print(error)
}
}
Conclusion
While I’m still learning about structured concurrency and loving the experience so far, it was fun to create a framework that only used the new syntax! I created Quoting to experiment with it, and I think it goes well with SwiftUI and the task
modifier.
If you have a better approach, please tag @rudrankriyam on Twitter! I love constructive feedback and appreciate constructive criticism.
Thanks for reading, and I hope you’re enjoying it!
Exploring Freelancing
Navigate freelancing as a developer; find clients, manage contracts, ensure timely payment, and learn from experiences!