👀

SwiftUIのTransitionをカスタマイズする

2024/08/22に公開

はじめに

アプリのインターフェース上で新しくTODOを作成したり、TODOが完了したら画面から削除する際に、画面上の要素が追加または削除されます。
Transitionを使って画面が変わるときに適切なアニメーション(トランジション)を設定すること、アプリの中で何か変化が起きたときに、画面がなぜ変わったのか、何が変わったのかがわかりやすくなります。

https://youtu.be/alhFwkbsxrs?si=d-ELfmOYTKz1W_5-&t=554

私たちのインターフェースは、アプリが舞台裏で何をしているかを知るためのポータルであり、トランジションは、起こっている変化を伝える便利な方法です。
トランジションは、新しいビューを表示したり、不要になったビューを削除したりする場合に便利です。
これらは、何が変更されたのか、なぜ変更が発生したのかについてのコンテキストを提供するのに役立ちます。

シンプルなトランジション

public func transition<T>(_ transition: T) -> some View where T : Transition

transitionというViewModifierをViewに対して適用します。

if showAvator {
    // スケールが変化しながら表示・削除される
    avatorView()
        .transition(.scale)
}

if showAvator {
    // 透明度が変化しながら表示・削除される
    avatorView()
        .transition(.opacity)
}

combined(with:)を組み合わせて複数のTransitionを適用することもできます。

if showAvator {
    // スケールと透明度が同時に変化しながら表示・削除される
    avatorView()
        .transition(.scale.combined(with: .opacity))
}    

iOS17からは、新しいTransitionプロトコルに準拠することで、もっと細かく制御できます。

カスタムViewModifierのように、func body(content: Content, phase: TransitionPhase) -> some View {内でビューを制御できます。

struct Twirl: Transition {
    func body(content: Content, phase: TransitionPhase) -> some View {
        content
            .scaleEffect(phase.isIdentity ? 1 : 0.5)
            .opacity(phase.isIdentity ? 1 : 0)
            .blur(radius: phase.isIdentity ? 0 : 20)
            .rotationEffect(
                .degrees(
                    phase == .willAppear ? 360 :
                        phase == .didDisappear ? -360 : 0
                )
            )
            .brightness(phase == .willAppear ? 1.0 : 0)
    }
}

TransitionPhaseenumになっていて、ビューが表示されようとしているか、削除されようとしているのかをswitchで分岐することができます。

enum TransitionPhase {
    case identity
    case willAppear
    case didDisappear
}

phase.isIdentity: Bool

使用例

struct CustomTransitionDemo: View {
    @State private var showAvator = false

    var body: some View {
        VStack {
            Spacer()
            if showAvator {
                avatorView()
                    .transition(Twirl())
            }
            Spacer()

            Button {
                withAnimation(.spring) {
                    showAvator.toggle()
                }
            } label: {
                Text("Toggle Avator")
            }
        }
    }

    func avatorView() -> some View {
        Image( ... ) // アイコン画像など
            .resizable()
            .aspectRatio(contentMode: .fill)
            .frame(width: 100, height: 100)
            .clipShape(.circle)
    }
}

参考リンク

https://youtu.be/alhFwkbsxrs?si=d-ELfmOYTKz1W_5-&t=554

https://developer.apple.com/documentation/swiftui/view/transition(_:)-5h5h0

https://developer.apple.com/documentation/swiftui/anytransition/combined(with:)

Discussion