5
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-07-17

はじめに

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

デモ

Simulator Screen Recording - iPhone 14 Pro - 2023-07-17 at 09.38.45.gif

コード

enumでErrorTypeを定義

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で使いやすくする

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が必要です。
そのため、@Binding errorTypeを元に、isPresentedDefaultError/isPresentedMultopleButtonErrorをBinding型で定義しています。

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で保持しておくのがポイントです

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全体が再描画される?と思っています。(詳しくはこちらなど参照)
なので、泣く泣くフラグを二つで管理する羽目となりました。

    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を保持しておけば、テストを書けるというメリットもあり!
間違っている箇所等あればこっそり教えてください!!

5
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?