はじめに
大規模アプリの開発を行なっていると、エラーアラートを出しわけしたいときが出てくると思います。
ex. APIのレスポンスによって文言を出し分けたい、ボタンの文言、タップ時の挙動を出し分けたい
こういった時のための便利なextensionを生やしました!!
デモ
コード
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を保持しておけば、テストを書けるというメリットもあり!
間違っている箇所等あればこっそり教えてください!!