SwiftUIでカスタムダイアログを実装したが閉じる時にアニメーションしない

2023.12.11

アプリ開発ではカスタムダイアログがよく実装される。プラットフォームごとに標準で用意されているAlertやActionSheetもあるが、それでは表現できないUIやブランドやプロダクトの「色」を出したい場合に実装される。

SwiftUIでカスタムダイアログを実装したが、閉じる際にフェードアウトアニメーションされずにパッと消えてしまう問題が発生した。本記事では、カスタムダイアログの基本的な実装と、特定の状況でアニメーションされない問題への対処方法について紹介する。

よくあるカスタムダイアログの実装

SwiftUIでカスタムダイアログを実装する際の基本的なステップは以下の通りだ。

  1. ダイアログとして表示するViewをカスタム定義する
  2. ダイアログの表示状態を管理するための変数(通常は@Stateプロパティ)を用意する
  3. ダイアログを表示および非表示にするためのトリガーを実装する

本記事ではカスタムダイアログの詳しい実装方法については割愛する。詳細についてはリルオッサ氏の「【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階層の構成に起因する問題である可能性がある。

ダイアログを閉じる時にアニメーションせずにパッと消える

たとえば、CustomDialogListなどの他のビューに隠れてしまうと、フェードアウトアニメーションが見えなくなる。これは最適化によって、変更があったViewのみを再描画するためで、結果としてダイアログが閉じる時のアニメーションが省略されてしまっている。

zIndex を指定してダイアログを前面に配置する

この問題の解決策として.zIndexの指定が有効だ。.zIndexを適用すると、CustomDialogが他のViewよりも前面に来ることが保証され、フェードアウトアニメーションが正しく表示されるようになる。

if isPresented {
    CustomDialog(isPresented: $isPresented)
        .transition(.opacity)
        .zIndex(10) // zIndexを指定
}

以下の動画では、Closeボタンをタップしたあとにフェードアウトアニメーションしていることがわかる。

ダイアログを閉じる時にアニメーションするようになった

ViewのzIndexの値は0である。.zIndex(10)を指定したことにより、ダイアログがView階層の最前面に配置され、他のViewとの重複によって表示されなかったアニメーションの問題が解消された。

参照記事