【SwiftUI】.alertをフラグなしで利用する

## はじめに
大規模アプリの開発を行なっていると、エラーアラートを出しわけしたいときが出てくると思います。
ex. APIのレスポンスによって文言を出し分けたい、ボタンの文言、タップ時の挙動を出し分けたい
こういった時のための便利なextensionを生やしました！！

## デモ
![Simulator Screen Recording - iPhone 14 Pro - 2023-07-17 at 09.38.45.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529173/3e81c27e-1603-5d14-55a0-e7d40a489623.gif)

## コード
#### enumでErrorTypeを定義
```swift
enum ErrorType {
    case defaultError(viewData: DefaultErrorViewData)
    case multipleButtonError(viewData: MultipleButtonErrorViewData)
    
    struct DefaultErrorViewData {
        let title: String
        let message: String
        let buttonText: String
        let handler: (() -> Void)?
    }
    
    struct MultipleButtonErrorViewData {
        let title: String
        let message: String
        let primaryButtonText: String
        let secondaryButtonText: String
        let primaryButtonHandler: (() -> Void)?
        let secondaryButtonHandler: (() -> Void)?
    }
}
```

#### enumのextensionを定義しErrorAlertModifierで使いやすくする
```swift
extension ErrorType {
    var isDefaultError: Bool {
        switch self {
        case .defaultError:
            return true
        default:
            return false
        }
    }
    
    var isMultipleButtonError: Bool {
        switch self {
        case .multipleButtonError:
            return true
        default:
            return false
        }
    }
}
```
#### Modifierを定義
SwiftUIのalert()モディファイアは、表示/非表示にisPresented: Binding<Bool>が必要です。
そのため、＠Binding errorTypeを元に、isPresentedDefaultError/isPresentedMultopleButtonErrorをBinding<Bool>型で定義しています。

```swift
struct ErrorAlertModifier: ViewModifier {
    @Binding var errorType: ErrorType?

    var isPresentedDefaultError: Binding<Bool> {
        Binding<Bool>(
            get: { errorType?.isDefaultError ?? false },
            set: {
                if !$0 {
                    errorType = nil
                }
            }
        )
    }
    
    var isPresentedMultopleButtonError: Binding<Bool> {
        Binding<Bool>(
            get: { errorType?.isMultipleButtonError ?? false },
            set: {
                if !$0 {
                    errorType = nil
                }
            }
        )
    }
    
    func body(content: Content) -> some View {
        content
            .alert(
                defaultErrorViewData?.title ?? "エラー",
                isPresented: isPresentedDefaultError
            ) {
                Button(defaultErrorViewData?.buttonText ?? "OK") {
                    defaultErrorViewData?.handler?()
                    errorType = nil
                }
            } message: {
                Text(defaultErrorViewData?.message ?? "")
            }
            .alert(
                multipleButtonErrorViewData?.title ?? "エラー",
                isPresented: isPresentedMultopleButtonError
            ) {
                Button(multipleButtonErrorViewData?.secondaryButtonText ?? "キャンセル") {
                    multipleButtonErrorViewData?.secondaryButtonHandler?()
                    errorType = nil
                }
                Button(multipleButtonErrorViewData?.primaryButtonText ?? "OK") {
                    multipleButtonErrorViewData?.primaryButtonHandler?()
                    errorType = nil
                }
            } message: {
                Text(multipleButtonErrorViewData?.message ?? "")
            }
    }
}

extension ErrorAlertModifier {
    var defaultErrorViewData: ErrorType.DefaultErrorViewData? {
        switch errorType {
        case .defaultError(let viewData):
            return viewData
        default:
            return nil
        }
    }
    
    var multipleButtonErrorViewData: ErrorType.MultipleButtonErrorViewData? {
        switch errorType {
        case .multipleButtonError(let viewData):
            return viewData
        default:
            return nil
        }
    }
}

``` 

#### 呼び出し元
errorTypeを＠Stateで保持しておくのがポイントです

```swift
struct ContentView: View {
    @State private var errorType: ErrorType?
    
    var body: some View {
        VStack {
            Button("ボタンが一つのエラーダイアログを出す") {
                errorType = .defaultError(
                    viewData: .init(
                        title: "ボタンが一つ",
                        message: "エラーメッセージ",
                        buttonText: "OKボタン",
                        handler: {
                            
                        }
                    ))
            }
            
            Button("ボタンが二つのエラーダイアログを出す") {
                errorType = .multipleButtonError(
                    viewData: .init(
                        title: "ボタンが二つ",
                        message: "エラーメッセージ",
                        primaryButtonText: "OK",
                        secondaryButtonText: "Cancel",
                        primaryButtonHandler: {
                            
                        },
                        secondaryButtonHandler: {
                            
                        })
                )
            }
        }
        .errorAlert(errorType: $errorType)
    }
}
```

## 少し解説
ErrorAlertModifierのbody内で、errorTypeを元にSwitch分で分岐すれば、下記のように不要なextensionなしで書けそうです。（フラグも一個になる）
こちら、一見うまく行きそうに見えますが、アラートのボタンを押すたびにviewの切り替えが入ってしまい、content全体が再描画される？と思っています。（詳しくは[こちらなど](https://zenn.dev/kntk/articles/4e3538f402d171)参照）
なので、泣く泣くフラグを二つで管理する羽目となりました。

```swift
    var isPresented: Binding<Bool> {
         Binding<Bool>(
             get: { errorType != nil },
             set: {
                 if !$0 {
                     errorType = nil
                 }
             }
         )
     }
    
    func body(content: Content) -> some View {
        switch errorType {
        case .defaultError(let viewData):
            content
                .alert("エラー", isPresented: isPresented) {
                    Button(viewData.buttonText) {
                        viewData.handler?()
                        errorType = nil
                    }
                } message: {
                    Text(viewData.message)
                }
        case .multipleButtonError(let viewData):
            content
                .alert("エラー", isPresented: isPresented) {
                    Button(viewData.secondaryButtonText) {
                        viewData.secondaryButtonHandler?()
                        errorType = nil
                    }
                    Button(viewData.primaryButtonText) {
                        viewData.primaryButtonHandler?()
                        errorType = nil
                    }
                } message: {
                    Text(viewData.message)
                }
        case nil:
            content
        }
    }
```



## まとめ
上記モディファイアを作成することにより、呼び出しもとからはerrorTypeを変更する（値を入れる）だけでalertの出し分けを行えるようになりました。
呼び出しもとのViewにいろんな.alertが生えなくて見栄えも良くみえます！
PresenterやViewModelでerrorTypeを保持しておけば、テストを書けるというメリットもあり！
間違っている箇所等あればこっそり教えてください！！