Open1

SwiftUI: Form/ListとwithAnimationの相性が悪い件

kabeyakabeya

Viewに要素を入れたり抜いたりできますが、その際、withAnimationを使ってオンオフを切り替えると出し入れの様子をアニメーション化できます。

ただ、これがFormList内だときれいになりません。

import SwiftUI

struct ToggleView: View {
    @State var showGlobe: Bool = true
    
    var body: some View {
        VStack {
            Text("Hello, world!")
            if showGlobe {
                Image(systemName: "globe")
                    .imageScale(.large)
                    .foregroundStyle(.tint)
            }
            Button("Toggle") {
                withAnimation {
                    showGlobe.toggle()
                }
            }
            .buttonStyle(.bordered)
        }
        .padding()
    }
}

struct ContentView: View {
    var body: some View {
        VStack {
            ToggleView()
                .background(.yellow)
            //Form {
            List {
                ToggleView()
                    .background(Color(white:0.8))
            }
            ToggleView()
                .background(.green)
            ToggleView()
                .background(.orange)
        }
        
    }
}

#Preview {
    ContentView()
}

実際の動きの様子が以下。真ん中のグレー部分がForm/Listのところです。

上とか下に配置したビューは、表示・非表示の切り替えにともなってスムーズにアニメーションしますが、Form/List内に配置したビューは、真ん中を中心にアニメーションしてしまううえにビューの外枠と中身とでアニメーション速度がずれてしまいます。

List内のビューのサイズ(この場合は高さ)を指定します。

            List {
                ToggleView()
                    .background(Color(white:0.8))
                    .frame(height: 150)
            }

こうすると、まだ真ん中を中心にアニメーションしたままですが、外枠の変な動き・ずれがなくなります。

さらに、内側ビュー内のVStackSpacer()を追加したうえ最大高さを指定します。

struct ToggleView: View {
    @State var showGlobe: Bool = true
    
    var body: some View {
        VStack {
            Text("Hello, world!")
            if showGlobe {
                Image(systemName: "globe")
                    .imageScale(.large)
                    .foregroundStyle(.tint)
            }
            Button("Toggle") {
                withAnimation {
                    showGlobe.toggle()
                }
            }
            .buttonStyle(.bordered)
            Spacer()
        }
        .padding()
        .frame(maxHeight: 130)
    }
}

こうすることで、List外のビューと同じように上部にアンカーがあるような動きにできます。

ただしサイズを適切に指定しないと、やっぱり中心からアニメーションする動きになります。
要素が可変でサイズが決めにくいときはどうするのが良いのかしら。