Xcode's refactoring options for async/await
Automatically adopt async functions in your codebase with ease
Xcode may offer up to three refactoring options when right-clicking on a completion handler-based function:
- Convert Function to Async
- Add Async Alternative
- 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!