Errorをいい感じにUIAlertControllerで表示する


iOSでエラーが起きた場合、UIAlertControllerに表示することは珍しくないですが、その際に発生するErrorを元にしていい感じにUIAlertControllerに表示します。

「いい感じ」の定義

「いい感じ」の定義としては、macOSで使われているAppKitのNSAlertを参考にします。
NSAlertというのはこんな感じのやつです。

NSAlertの中にはmessageText,informativeTextが表示され、ボタンが存在します。
大体UIAlertControllerのtitle,messageと同じ形に見えませんか?

NSAlertにはErrorを渡して生成することによって、これらの要素が自動でつくので、同様にUIAlertControllerでも、Errorを渡すだけで各種パラメータが設定されるようにします。

UIAlertControllerの表示

NSAlertのmessageTextは、NSErrorのlocalizedDescriptioninformativeTextにはNSErrorのlocalizedRecoverySuggestionが使われています。よって、ErrorをNSErrorにキャストしてこれらのパラメータを割り当てればOKです。

        let nsError = error as NSError
        let alert = UIAlertController(title: nsError.localizedDescription,
                                      message: nsError.localizedRecoverySuggestion,
                                      preferredStyle: .alert)

LocalizedErrorを生成する

実際にNSErrorのパラメータがわかったところで、生成されるエラーがこの仕様に準拠していなければ意味がありませんので、この仕様に準拠したエラーを生成します。

NSErrorを生成し、NSLocalizedDescriptionKeyなどをUserInfoに設定するのもいいのですが、Swiftであれば LocalizedError を使うことで準拠が可能です。

LocalizedErrorに含まれる、errorDescription は、NSErrorにキャストするとlocalizedDescriptionに、recoverySuggestionlocalizedRecoverySuggestionになります。

enum MyError: LocalizedError {
    case error1

    var errorDescription: String? { return "errorDescription" }
    var recoverySuggestion: String? { return "recoverySuggestion" }
}

RecoverableErrorでリカバリー対応する

NSAlertにRecoverableErrorに適合したエラーを渡して生成すると、リカバリーオプションボタンのついたNSAlertを生成できます。
これらのボタンを押下すると、RecoverableError内で実装したリカバリー処理が動くので、こちらもUIAlertControllerに再現してみましょう。

先ほどのLocalizedErrorも含めて、macOSのpresentErrorのように、UIViewControllerのクラスでエラーを表示できるextensionを作成してみました。
delegatedidRecoverSelector を設定することでリカバリー時のreturnを取得することができます。

extension UIViewController {
    func presentError(_ error: Error,
                      delegate:Any? = nil,
                      didRecoverSelector:Selector? = nil,
                      contextInfo: UnsafeMutableRawPointer? = nil) {
        let nsError = error as NSError
        let alert = UIAlertController(title: nsError.localizedDescription,
                                      message: nsError.localizedRecoverySuggestion,
                                      preferredStyle: .alert)

        if let localizedRecoveryOptions = nsError.localizedRecoveryOptions,
            !localizedRecoveryOptions.isEmpty,
            let attempter: AnyObject = nsError.recoveryAttempter as AnyObject? {

            for (i, value) in localizedRecoveryOptions.enumerated() {
                let alertAction = UIAlertAction(title: value, style: .default) { _ in
                    attempter.attemptRecovery(fromError: error,
                                              optionIndex: i,
                                              delegate: delegate,
                                              didRecoverSelector: didRecoverSelector,
                                              contextInfo: contextInfo)
                }
                alert.addAction(alertAction)
            }
        } else {
            alert.addAction(UIAlertAction(title: NSLocalizedString("ok", value: "OK", comment: "エラーアラートのデフォルトボタン"), style: .default, handler: nil))
        }
        present(alert, animated: true, completion: nil)
    }
}

追記:presentErrorの使い方

class ViewController: UIViewController {

    @IBAction func showError(_ sender: Any) {
        presentError(MyLocalizedError.error1)
    }

    @IBAction func showRecoverableError(_ sender: Any) {
        // リカバリー付きエラーを表示
        presentError(MyRecoverableError.error1, delegate: self, didRecoverSelector: #selector(didPresentErrorWithRecovery(_:contextInfo:)), contextInfo: nil)
    }

    /// リカバリーの結果が返ってくる
    /// - Parameters:
    ///   - didRecover: attemptRecoveryの成功有無
    ///   - contextInfo: エラー回復の試行に関連する任意のデータ
    @objc func didPresentErrorWithRecovery(_ didRecover:Bool, contextInfo:UnsafeMutableRawPointer?) {
    }
}

見比べてみる

macOS iOS
Error
LocalizedError
LocalizedError & RecoverableError

まとめ

つかおう!LocalizedError!