iTranslated by AI
Using UIAlertController with Swift Concurrency
When chaining asynchronous tasks, you may want to check a value with the user before deciding whether to continue.
In such cases, you prompt the user for a choice via a UIAlertController or similar as follows.
For example, a workflow that fetches the latest terms of use and transitions to the sign-in screen if the user agrees can be written like this:
func termsOfUseWorkflow() async throws {
let termsOfUse = try await latestTermsOfUse()
presentAlert(termsOfUse)
}
func presentAlert(termsOfUse) {
let alert = UIAlertController(termsOfUse)
alert.addAction("Accept") {
acceptedTermsOfUse()
}
alert.addAction("Cancel") {
}
present(alert)
}
func acceptedTermsOfUse() {
Task {
await signin()
...
}
}
Using Concurrency makes the flow of processing easier to understand.
However, as shown above, when a UIAlertController is involved, the Task becomes separated, which reduces readability.
Let's try to execute everything within a single Task by wrapping UIAlertController as follows.
protocol UIAlertActionStylePresentable {
var style: UIAlertAction.Style { get }
}
extension UIAlertController {
@MainActor
static func choice<T: RawRepresentable>(
title: String?,
message: String?,
preferredStyle: UIAlertController.Style = .alert,
animated: Bool = true,
presentation: UIViewController
) async -> T where T: CaseIterable, T: CustomStringConvertible, T: UIAlertActionStylePresentable {
await withCheckedContinuation { continuation in
let alert = UIAlertController(title: title, message: message, preferredStyle: preferredStyle)
for `case` in T.allCases {
let action = UIAlertAction(
title: `case`.description,
style: `case`.style,
handler: { _ in
continuation.resume(returning: `case`)
}
)
alert.addAction(action)
}
presentation.present(alert, animated: animated)
}
}
}
By using this wrapper, you can rewrite the initial process as follows.
Since the choices are passed as an enum, let's prepare that first.
enum Choice: String, CaseIterable, CustomStringConvertible, UIAlertActionStylePresentable {
case accept
case cancel
var description: String {
switch self {
case .accept:
return "Accept"
case .cancel:
return "Cancel"
}
}
var style: UIAlertAction.Style {
switch self {
case .accept:
return .default
case .cancel:
return .cancel
}
}
}
The process can be rewritten as follows:
func termsOfUseWorkflow() async throws {
let termsOfUse = try await latestTermsOfUse()
let choice: Choice = await UIAlertController.choice(
termsOfUse: termsOfUse,
presentation: self
)
switch choice {
case .accept:
await signin()
...
case .cancel:
break
}
}
With this, asynchronous processing and user operations can now be represented as a single sequence of Tasks.
While it seems versatile, caution is needed because if a way to close the UIAlertController without triggering the cancel handler were to be found, it would become impossible to exit.
Discussion