Adding Concurrency in Swift Package
Book

Exploring MusicKit and Apple Music API

Unlock the full power of MusicKit & Apple Music APIs in your apps with the best guide! Use code musickit-blog for a limited-time 35% discount!

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!

Book

Exploring MusicKit and Apple Music API

Unlock the full power of MusicKit & Apple Music APIs in your apps with the best guide! Use code musickit-blog for a limited-time 35% discount!

Written by

Rudrank Riyam

Hi, my name is Rudrank. I create apps for Apple Platforms while listening to music all day and night. Author of "Exploring MusicKit". Apple WWDC 2019 scholarship winner.