UIAlertController の async/await 対応
1. UIAlertController を async/await で簡単に扱いたい!
AlertController は、Swift Concurrency を活用し、UIAlertController
の表示とユーザーからのアクション取得を簡潔に実現するライブラリです。
主な特徴は以下のとおりです。
-
async/await に対応
アラートの表示からユーザーのアクション取得までを async/await でシンプルに扱えるようにしました。 -
alert と actionSheet に対応
アラートとアクションシートの両方に対応します。iPad 環境での popover 設定も可能です。 -
アクティビティインジケーター的な使い方
ユーザーのアクションを待たずに、表示した時点でレスポンスが返るpresent
も用意しました。
waitForDismissOrAction
で操作を待つことも可能です。
呼び出しサンプル
@objc func showBasicAlert() {
Task {
let alert = AlertController(
title: "Confirmation",
message: "Do you want to proceed?",
preferredStyle: .alert,
dismissResult: false
)
alert.addAction(AlertAction(title: "Proceed", style: .default, result: true), isPreferred: true)
alert.addAction(AlertAction(title: "Cancel", style: .cancel, result: false))
let result = await alert.presentAndWait(on: self, animated: true)
print("Basic Alert result: \(result)")
}
}
UIAlertController
を async/await で扱えるのはとても便利なのでおすすめです。
この記事では、似た機能を自作したい人向けに、設計で考慮したポイントをまとめています。
2. 設計で考慮したポイント
2.1 AlertAction によるボタンと結果の紐付け
通常なら UIAlertAction
のhandlerでボタンを押されたときの処理を行います。
async/awaitを使う場合、非同期メソッドの戻り値として、ボタンごとに異なる結果を返せるようにするための仕組みが必要です。
そこで、AlertAction<T>
を定義し、ボタンのタイトル・スタイル・結果 (T
) を紐付けるようにしました。
public struct AlertAction<T: Sendable> {
public let title: String
public let style: UIAlertAction.Style
public let result: T
}
アクションごとに result
を持たせて、ボタンが押されたときは result
を返すようにしました。
dismissResult
によるキャンセル時の結果の扱い
2.2 ユーザーがボタンを押さずにアラートが閉じられることもあります。
例えば以下のようなケースです。
-
.dismiss(animated:)
を呼び出して UIAlertController を閉じた場合 -
.cancel
ボタンを追加せずにactionSheet
を表示し、枠外タップで閉じられた場合
このような場合でも呼び出し元が結果を確実に受け取れるよう、AlertControllerの初期化時に dismissResult
を受け取るようにしました。
ボタンが押されずにアラートが閉じられた際に dismissResult
を返すことで、処理が確実に進行するようにしています。
setOnDismissHandler
によるリソース管理と AlertController の継承
2.3 アラートが閉じられたことを検知する方法として、popoverPresentationController.delegate
を使う手もあります。
しかし、これを直接利用すると、ユーザーが popoverPresentationController.delegate
を設定した場合に上書きされ、アラートが閉じたことを検出できなくなる問題があります。
そこで、UIAlertController
を継承した DismissAwareAlertController
を作成し、deinit
で確実にハンドラーを呼び出す方式を採用しました。
private class DismissAwareAlertController: UIAlertController {
var onDismissHandler: (@Sendable () -> Void)?
deinit {
onDismissHandler?()
}
func setOnDismissHandler(_ handler: @escaping @Sendable () -> Void) {
self.onDismissHandler = handler
}
}
この設計により、AlertController
側で setOnDismissHandler
を用意し、どのような形でアラートが閉じられても確実に後処理が実行されるようになっています。
DismissAwareAlertController
を weak で保持
2.3.1 アラートの表示後は アラートを表示するまでは retainedAlertController
に強い参照を持たせていますが、表示後は presentedAlertController
に weak
で保持するようにしています。
self.presentedAlertController = self.retainedAlertController
self.retainedAlertController = nil
これにより、アラートが閉じられた際に DismissAwareAlertController
が適切に解放され、メモリリークを防ぎます。
2.3.2 delegate を橋渡しする方法について
popoverPresentationController.delegate
を上書きされないようにするために、外部から設定される delegate
を保持して、橋渡しする方法も考えられます。
この方法は UIPopoverPresentationControllerDelegate
に含まれる複数のメソッドをすべて処理する必要があり、メソッド数も多いため今回は採用しませんでした。
presentAndWait
と present
を両方用意した理由
2.4 presentAndWait
は、アラートの表示とユーザーのアクション待ちを1つの非同期メソッドで完結できて使いやすいです。
しかしインジケーター的な使い方をしたい場合もあると思うので表示だけする present
を用意しました。
// アラートを表示するが、即時に次の処理を続行
let alert = AlertController(
title: "Processing...",
message: "Please wait",
preferredStyle: .alert,
dismissResult: nil
)
await alert.present(on: self, animated: true)
// 他の処理を行う(例: ネットワークリクエストなど)
let result = await someAsyncOperation()
// アラートを閉じる
await alert.dismiss(animated: true)
これにより、アラートを開いたまま非同期処理を待ち、完了後に閉じるといった流れをシンプルに記述できます。
また present
した場合もユーザー操作を受け取れるように waitForDismissOrAction
を用意しました。
-
waitForDismissOrAction
のキャンセル待ち - 時間のかかる処理(ネットワークリクエストなど)
を並行して動作させて、ユーザーアクションによるキャンセルを実装することもできます。
3. その他
コンパクトなソースコードですので、疑問点があれば直接見ていただくと早いと思います。
同等のものを自作して、以下のような変更を加えても良いと思っています。
- 不要な部分を削る
- キャンセル時は
CancellationError
を投げて欲しい -
UIAlertController
はアクティビティインジケーターとして使えないようにする - テキスト入力の対応
Discussion