🔔

SwiftUIのAlertを少し使いやすくする

2022/05/22に公開

最近SwiftUIとMVVMでアプリを作成しているのですが、AlertでViewが肥大化してしまい見通しが悪かったので少し改善してみました。

環境

Xcode 13.0 (13A233)
Swift 5.5

通常の実装例

通常の実装例がこちらになります。
実際にはMVVM想定なのでCombineを使っていますが、今回は簡略化のためにある程度省略しています。

struct ContentView: View {
    @ObservedObject var viewModel: ContentViewModel
    
    var body: some View {
        Button("アラートを表示する") {
            viewModel.isShowingAlert = true
        }
        .alert("タイトル", isPresented: $viewModel.isShowingAlert) {
            Button("OK") {
                // do something
            }
        } message: {
            Text("詳細メッセージ")
        }
    }
}

class ContentViewModel: ObservableObject {
    @Published var isShowingAlert: Bool = false
}

この実装だと、画面ごとにAlertを表示するフラグやタイトル、メッセージなどを格納する変数を用意する必要があり、またactionやmessageなどでViewのコードも肥大化しがちになります。

改善した実装例

改善したコードがこちらになります。
ボタンが1つもしくは2つの2パターンを想定して実装しています。

フラグやtitleなどを管理するEntity

Alertを表示するフラグやタイトル、ボタンタップ時の動作を定義したEntityを用意します。

enum AlertType {
    case single
    case double
}

struct AlertEntity {
    var isShowingAlert: Bool = false
    var alertType: AlertType = .single
    var title: String = ""
    var message: String = ""
    var buttonAction: () -> Void = {}
    
    mutating func show(alertType: AlertType = .single, title: String, message: String, buttonAction: @escaping () -> Void = {}) {
        self.alertType = alertType
        self.title = title
        self.message = message
        self.buttonAction = buttonAction
        
        self.isShowingAlert = true
    }
    
    mutating func hide() {
        self.isShowingAlert = false
    }
}

Alertを共通化したViewModifier

デフォルトのままalertを実装するとViewが肥大化するのでViewModifierで共通化します。
extensionは簡単に適用できるように用意したものなので無くても大丈夫です。

struct CustomAlertView: ViewModifier {
    @Binding var alertEntity: AlertEntity
    func body(content: Content) -> some View {
        content
            .alert(isPresented: $alertEntity.isShowingAlert) {
                switch alertEntity.alertType {
                case .single:
                    return Alert(title: Text(alertEntity.title),
                          message: Text(alertEntity.message),
                          dismissButton: .default(Text("閉じる"), action: {
                        alertEntity.buttonAction()
                    }))
                case .double:
                    return Alert(title: Text(alertEntity.title),
                          message: Text(alertEntity.message),
                          primaryButton: .default(Text("いいえ")),
                          secondaryButton: .destructive(Text("はい"), action: {
                        alertEntity.buttonAction()
                    }))
                }
            }
    }
}

extension View {
    func customAlert(for alertEntity: Binding<AlertEntity>) -> some View {
        modifier(CustomAlertView(alertEntity: alertEntity))
    }
}

使用例

ViewModelに先程記載したEntityを用意し、任意のViewに独自に適宜したCustomAlertViewを適用させます。
その際にViewModelで定義したEntityをBindingさせておきます。
あとはAlertを呼び出したいところでalertEntityのshowメソッドを発火させればOKです。

struct ContentView: View {
    @ObservedObject var viewModel: ContentViewModel
    
    var body: some View {
        Button("アラートを表示する") {
            viewModel.alertEntity.show(title: "警告", message: "要素が空です。")
        }.customAlert(for: $viewModel.alertEntity)
    }
}

class ContentViewModel: ObservableObject {
    @Published var alertEntity: AlertEntity = AlertEntity()
}

参考

https://developer.apple.com/documentation/swiftui/view/alert(_:ispresented:presenting:actions:message:)-8584l

https://www.swiftbysundell.com/tips/creating-custom-swiftui-container-views

https://zenn.dev/spyc/articles/993fb47a1d42e8

Discussion