アプリ開発ではカスタムダイアログがよく実装される。プラットフォームごとに標準で用意されているAlertやActionSheetもあるが、それでは表現できないUIやブランドやプロダクトの「色」を出したい場合に実装される。
SwiftUIでカスタムダイアログを実装したが、閉じる際にフェードアウトアニメーションされずにパッと消えてしまう問題が発生した。本記事では、カスタムダイアログの基本的な実装と、特定の状況でアニメーションされない問題への対処方法について紹介する。
よくあるカスタムダイアログの実装
SwiftUIでカスタムダイアログを実装する際の基本的なステップは以下の通りだ。
- ダイアログとして表示するViewをカスタム定義する
- ダイアログの表示状態を管理するための変数(通常は
@State
プロパティ)を用意する - ダイアログを表示および非表示にするためのトリガーを実装する
本記事ではカスタムダイアログの詳しい実装方法については割愛する。詳細についてはリルオッサ氏の「【SwiftUI】ポップアップで表示されるダイアログを作ってみた」を参照して欲しい。以下はよくあるカスタムダイアログの実装である。
struct CustomDialog: View {
@Binding var isPresented: Bool
var body: some View {
ZStack {
Color.black.opacity(0.6)
.ignoresSafeArea(.all)
VStack(spacing: 16) {
Text("Title")
Text("Message")
Button("Close") {
withAnimation {
isPresented = false
}
}
}
.padding()
.frame(width: 280)
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
}
下図のようによくあるダイアログが実装できた。背景が半透明の黒色で、前面に角丸の白色のベースがあり、タイトル、本文、閉じるボタンの要素が存在している。
このカスタムダイアログを利用するため、親Viewである ContentView
にダイアログの表示を管理する変数 isPresented
を追加し、セルがタップされたらダイアログを表示させるようにした。
struct ContentView: View {
@State private var isPresented = false
var body: some View {
ZStack {
List {
ForEach(0 ..< 20) { e in
Button("\(e)") {
withAnimation {
isPresented.toggle()
}
}
}
}
if isPresented {
CustomDialog(isPresented: $isPresented)
.transition(.opacity)
}
}
}
}
ダイアログを閉じる時にアニメーションしない
.transition(.opacity)
を指定しているので、ダイアログを開いた時にはフェードインアニメーションが適用される。しかし、ダイアログを閉じる時にフェードアウトアニメーションが適用されず、Closeボタンをタップした瞬間にダイアログがパッと消えてしまう。これは、View階層の構成に起因する問題である可能性がある。
たとえば、CustomDialog
がList
などの他のビューに隠れてしまうと、フェードアウトアニメーションが見えなくなる。これは最適化によって、変更があったViewのみを再描画するためで、結果としてダイアログが閉じる時のアニメーションが省略されてしまっている。
zIndex を指定してダイアログを前面に配置する
この問題の解決策として.zIndex
の指定が有効だ。.zIndex
を適用すると、CustomDialog
が他のViewよりも前面に来ることが保証され、フェードアウトアニメーションが正しく表示されるようになる。
if isPresented {
CustomDialog(isPresented: $isPresented)
.transition(.opacity)
.zIndex(10) // zIndexを指定
}
以下の動画では、Closeボタンをタップしたあとにフェードアウトアニメーションしていることがわかる。
ViewのzIndex
の値は0
である。.zIndex(10)
を指定したことにより、ダイアログがView階層の最前面に配置され、他のViewとの重複によって表示されなかったアニメーションの問題が解消された。