Xcode's refactoring options for async/await

Xcode's refactoring options for async/await

Automatically adopt async functions in your codebase with ease

Β·

3 min read

Xcode may offer up to three refactoring options when right-clicking on a completion handler-based function:

  1. Convert Function to Async
  2. Add Async Alternative
  3. Add Async Wrapper

I will explain the result of each refactoring option based on the example given below.

    func fetchRandomPictures(count: Int = 20, completion: @escaping (Result<[Picture], Error>) -> Void) {
        let url = pictureOfTheDayURL(additionalParameters: [
            "count": "\(count)"
        ])
        let request = URLRequest(url: url)
        let task = URLSession.shared.dataTask(with: request) {
            data, response, error in

            if let data = data {
                let decoder = JSONDecoder()
                do {
                    let pictures = try decoder.decode([Picture].self, from: data)
                    completion(.success(pictures))
                } catch {
                    completion(.failure(error))
                }
            } else if let error = error {
                completion(.failure(error))
            } else {
                completion(.failure("unknownFailure"))
            }
        }
        task.resume()
    }

Videos for each refactoring option are available in my related Twitter thread.

Convert Function to Async

The existing function gets replaced. This is a breaking change, i.e the function signature gets changed and you need to update all places that call the existing function!

    func fetchRandomPictures(count: Int = 20) async throws -> [Picture] {
        let url = pictureOfTheDayURL(additionalParameters: [
            "count": "\(count)"
        ])
        let request = URLRequest(url: url)
        return try await withCheckedThrowingContinuation { continuation in
            let task = URLSession.shared.dataTask(with: request) {
                data, response, error in

                if let data = data {
                    let decoder = JSONDecoder()
                    do {
                        let pictures = try decoder.decode([Picture].self, from: data)
                        continuation.resume(with: .success(pictures))
                    } catch {
                        continuation.resume(with: .failure(error))
                    }
                } else if let error = error {
                    continuation.resume(with: .failure(error))
                } else {
                    continuation.resume(with: .failure("unknownFailure"))
                }
            }
            task.resume()
        }
    }

Add Async Alternative

A new async function is added. The existing function signature stays as-is, but the function implementation was replaced to use the newly created async function.

    @available(*, renamed: "fetchRandomPictures(count:)")
    func fetchRandomPictures(count: Int = 20, completion: @escaping (Result<[Picture], Error>) -> Void) {
        Task {
            do {
                let result = try await fetchRandomPictures(count: count)
                completion(.success(result))
            } catch {
                completion(.failure(error))
            }
        }
    }

    func fetchRandomPictures(count: Int = 20) async throws -> [Picture] {
        let url = pictureOfTheDayURL(additionalParameters: [
            "count": "\(count)"
        ])
        let request = URLRequest(url: url)
        return try await withCheckedThrowingContinuation { continuation in
            let task = URLSession.shared.dataTask(with: request) {
                data, response, error in

                if let data = data {
                    let decoder = JSONDecoder()
                    do {
                        let pictures = try decoder.decode([Picture].self, from: data)
                        continuation.resume(with: .success(pictures))
                    } catch {
                        continuation.resume(with: .failure(error))
                    }
                } else if let error = error {
                    continuation.resume(with: .failure(error))
                } else {
                    continuation.resume(with: .failure("unknownFailure"))
                }
            }
            task.resume()
        }
    }

Add Async Wrapper

The existing function is unchanged, and a new async function is added that uses withCheckedThrowingContinuation to reuse the existing function.

    @available(*, renamed: "fetchRandomPictures(count:)")
    func fetchRandomPictures(count: Int = 20, completion: @escaping (Result<[Picture], Error>) -> Void) {
        let url = pictureOfTheDayURL(additionalParameters: [
            "count": "\(count)"
        ])
        let request = URLRequest(url: url)
        let task = URLSession.shared.dataTask(with: request) {
            data, response, error in

            if let data = data {
                let decoder = JSONDecoder()
                do {
                    let pictures = try decoder.decode([Picture].self, from: data)
                    completion(.success(pictures))
                } catch {
                    completion(.failure(error))
                }
            } else if let error = error {
                completion(.failure(error))
            } else {
                completion(.failure("unknownFailure"))
            }
        }
        task.resume()
    }

    func fetchRandomPictures(count: Int = 20) async throws -> [Picture] {
        return try await withCheckedThrowingContinuation { continuation in
            fetchRandomPictures(count: count) { result in
                continuation.resume(with: result)
            }
        }
    }

Conclusion

The least disruptive refactoring option is Add Async Wrapper which I recommend.

If you have full control over the call sites and don't need to provide compatibility, go with the most disruptive refactoring option Convert Function to Async.

Did you find this article valuable?

Support Marco Eidinger by becoming a sponsor. Any amount is appreciated!