ℹ️

UIAlertController の async/await 対応

2025/02/19に公開

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 を返すようにしました。


2.2 dismissResult によるキャンセル時の結果の扱い

ユーザーがボタンを押さずにアラートが閉じられることもあります。
例えば以下のようなケースです。

  • .dismiss(animated:) を呼び出して UIAlertController を閉じた場合
  • .cancel ボタンを追加せずに actionSheet を表示し、枠外タップで閉じられた場合

このような場合でも呼び出し元が結果を確実に受け取れるよう、AlertControllerの初期化時に dismissResult を受け取るようにしました。
ボタンが押されずにアラートが閉じられた際に dismissResult を返すことで、処理が確実に進行するようにしています。


2.3 setOnDismissHandler によるリソース管理と AlertController の継承

アラートが閉じられたことを検知する方法として、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 を用意し、どのような形でアラートが閉じられても確実に後処理が実行されるようになっています。

2.3.1 アラートの表示後は DismissAwareAlertController を weak で保持

アラートを表示するまでは retainedAlertController に強い参照を持たせていますが、表示後は presentedAlertControllerweak で保持するようにしています。

self.presentedAlertController = self.retainedAlertController
self.retainedAlertController = nil

これにより、アラートが閉じられた際に DismissAwareAlertController が適切に解放され、メモリリークを防ぎます。

2.3.2 delegate を橋渡しする方法について

popoverPresentationController.delegate を上書きされないようにするために、外部から設定される delegate を保持して、橋渡しする方法も考えられます。
この方法は UIPopoverPresentationControllerDelegate に含まれる複数のメソッドをすべて処理する必要があり、メソッド数も多いため今回は採用しませんでした。

2.4 presentAndWaitpresent を両方用意した理由

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