【SwiftUI】続・複数種類のアラート(alert)の表示/非表示を管理する

はじめに

先日、 YUMEMI.grow Mobile #19 にて「【SwiftUI】複数種類のアラート(alert)の表示/非表示を管理する」の内容を LT させていただきました。
その際、視聴いただいた方に「alert modifier は一つにまとめた方が良いかも」という主旨の感想をいただきました。
私としても「確かに」と思ったので、 alert modifier は一つにまとめつつ、複数のアラートの表示/非表示を管理する方法にも対応してみました。

本記事は ↓ の内容が前提となった記事なので、まずはそちらをご覧ください。

taji-taji.hatenablog.com

対応

まず、前回の記事で Binding に生やした isPresented() メソッドに変更を加えます。

 protocol AlertCase: Equatable {}
 protocol AlertStateRepresentable: Equatable {
   associatedtype Alert: AlertCase
   static var dismissed: Self { get }
   static func presenting(_ alert: Alert) -> Self
+  var isPresenting: Bool { get }
 }

 enum AlertState<Alert: AlertCase>: AlertStateRepresentable {
   case dismissed
   case presenting(Alert)
+
+  var isPresenting: Bool {
+    if case .presenting = self {
+      true
+    } else {
+      false
+    }
+  }
 }

 extension Binding where Value: AlertStateRepresentable {
-  func isPresented(_ target: Value.Alert) -> Binding<Bool> {
+  func isPresented(_ target: Value.Alert? = nil) -> Binding<Bool> {
     .init(
-      get: { wrappedValue == .presenting(target) },
+      get: {
+        if let target {
+          wrappedValue == .presenting(target)
+        } else {
+          wrappedValue.isPresenting
+        }
+      },
       set: { if !$0 { wrappedValue = .dismissed }}
     )
   }
 }

前回までは引数として判定対象となるアラートを取っていましたが、引数なしで呼び出せるようにしました。
内部的には引数なしの場合、単に「いずれかのアラートが表示状態である」という判定のみになっています。

(※ var isAnyAlertPresented: Binding<Bool> { get } とかにして既存のメソッドと分けた方が意図が明確になるかもしれませんが、一旦本記事ではこれでやってみます)

これを View 側で使ってみます。
比較対象として、前回記事の alert modifier が複数ある View 側のコードを再掲します。

import SwiftUI

struct ContentView: View {
  @State var alertState: AlertState<MyAlert> = .dismissed

  enum MyAlert: AlertCase {
    case alert1, alert2
  }

  var body: some View {
    VStack {
      Button("Alert 1") {
        alertState = .presenting(.alert1)
      }
      Button("Alert 2") {
        alertState = .presenting(.alert2)
      }
    }
    .alert(
      "Alert 1",
      isPresented: $alertState.isPresented(.alert1)
    ) {
      Button("Close", role: .cancel) {
        alertState = .dismissed
      }
    }
    .alert(
      "Alert 2",
      isPresented: $alertState.isPresented(.alert2)
    ) {
      Button("Close", role: .cancel) {
        alertState = .dismissed
      }
    }
  }
}

今回の変更を使って、 alert modifier を一つにしてみます。

struct ContentView: View {
  @State var alertState: AlertState<MyAlert> = .dismissed
  
  // ⭐️ alert の種類によってタイトルが変わるのでここで定義
  var alertTitle: String {
    switch alertState {
    case .presenting(.alert1):
      "Alert 1"
    case .presenting(.alert2):
      "Alert 2"
    case .dismissed:
      ""
    }
  }

  var body: some View {
    VStack {
      Button("Alert 1") { alertState = .presenting(.alert1) }
      Button("Alert 2") { alertState = .presenting(.alert2) }
    }
    // ⭐️ alertTitle を title 引数に渡す。
    // ⭐️ isPresented には引数なしで Binding.isPresented() を渡す。
    .alert(alertTitle, isPresented: $alertState.isPresented()) {
      // ⭐️ actions には alert ごとの action を定義する。
      switch alertState {
      case .presenting(.alert1):
        Button("Close (1)", role: .cancel) {
          alertState = .dismissed
        }
      case .presenting(.alert2):
        Button("Close (2)", role: .cancel) {
          alertState = .dismissed
        }
      case .dismissed:
        EmptyView()
      }
    }
  }
}

これで alert modifier を一つにしつつ、引き続き複数の種類の alert の表示/非表示の管理ができました。

前回の方法はいらない?

alert modifier を一つにまとめられたので、前回記事で書いた複数の alert modifier を用いた方法は不要になるようにも思います。
とはいえ、下記のようなケースもあるかと思うので、使えることもあるかなと思っています。

alert modifier をラップした独自の alert modifier を作っている場合

例えば、共通のエラーアラートとして通常の alert modifier をラップしたメソッドを作っていると仮定します。

extension View {
  func errorAlert(
    isPresented: Binding<Bool>,
    onClosed: @escaping () -> Void
  ) -> some View {
    self.alert("Error!!", isPresented: isPresented) {
      Button("Close", role: .cancel) {
        onClosed()
      }
    }
  }
}

このメソッドを用いたアラートと通常の alert modifier を用いたアラートの2種類のアラートが表示し得る画面がある場合は、前回の記事のような方法を使うのも手かもしれません。

struct ContentView: View {
  @State var alertState: AlertState<MyAlert> = .dismissed

  var body: some View {
    VStack {
      Button("Alert 1") { alertState = .presenting(.alert1) }
      Button("Alert 2") { alertState = .presenting(.alert2) }
    }
    .alert("Alert 1", isPresented: $alertState.isPresented(.alert1)) {
      Button("Close (1)", role: .cancel) {
        alertState = .dismissed
      }
    }
    .errorAlert(isPresented: $alertState.isPresented(.alert2)) {
      alertState = .dismissed
    }
  }
}

最後に

さて、今回は LT を機にフィードバックをいただくことができ、より深く考える機会となりました。
主催者の方、フィードバックをくださった方、ありがとうございます。
まだまだ模索中なところもあるので、引き続き色々と試していきたいと思います。