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

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

# 「いい感じ」の定義

https://developer.apple.com/documentation/appkit/nsalert

「いい感じ」の定義としては、macOSで使われているAppKitの`NSAlert`を参考にします。
NSAlertというのはこんな感じのやつです。
<img width="532" alt="messageText.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62305/7879ccb8-6735-f506-e33a-dfb416dd7a32.png">

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

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

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

```swift
        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`に、`recoverySuggestion`は`localizedRecoverySuggestion`になります。

```swift
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を作成してみました。
`delegate` や `didRecoverSelector` を設定することでリカバリー時のreturnを取得することができます。

```swift
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の使い方

```swift
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|<img width="532" alt="スクリーンショット 2020-05-07 9.06.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62305/c1335c87-d829-a115-eed9-84ee429988b6.png">|![Simulator Screen Shot - iPhone 11 Pro Max - 2020-05-07 at 09.05.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62305/b893f3f6-8377-96dd-dced-24bc1c483091.png)|
|LocalizedError|<img width="532" alt="スクリーンショット 2020-05-07 9.08.13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62305/c052d8da-313a-62e5-26ba-5fb26ef238fd.png">|![Simulator Screen Shot - iPhone 11 Pro Max - 2020-05-07 at 09.08.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62305/40686ccf-82a5-eab5-a65a-f3853e829eb4.png)|
| LocalizedError & RecoverableError|<img width="544" alt="スクリーンショット 2020-05-07 9.10.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62305/bfd05325-24c8-15eb-846b-58b0d0d65b6d.png">|![Simulator Screen Shot - iPhone 11 Pro Max - 2020-05-07 at 09.10.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62305/ecb6ebf0-6a96-a33a-7041-fd8dee52e662.png)|


# まとめ
つかおう！LocalizedError！