SwiftUI: List直下でswitch caseしたらアニメーションしなくなった

SwiftUI でリストを実装中にハマったので書きます。

最小限のコードだけ抜き出しました。重要なところはList(またはForEach)の直後にswitch caseがある点です。

このように実装すると withAnimation内でデータを追加してもアニメーションが発生しなくなります。

import SwiftUI

struct ContentView: View {
    enum Value: Identifiable {
        case int(Int)
        case float(Float)

        var id: String {
            switch self {
            case let .int(value): "int(\(value))"
            case let .float(value): "float(\(value))"
            }
        }
    }
    
    @StateObject var viewModel = ViewModel()

    var body: some View {
        List(viewModel.values) { v in
            // Listの直下でswitch caseするとアニメーションしなくなる
            switch v {
            case let .int(value):
                Text("Int: \(value)")
            case let .float(value):
                Text("Float: \(value)")
            }
        }
        .listStyle(.plain)
        .safeAreaInset(edge: .bottom) {
            Button("Add Top") {
                withAnimation {
                    viewModel.insert()
                }
            }
        }
    }
}

@MainActor
final class ViewModel: ObservableObject {
    @Published var values: [ContentView.Value] = [.int(0)]

    func insert() {
        values.insert(Bool.random() ? .int(values.count) : .float(Float(values.count)), at: 0)
    }
}

#Preview {
    ContentView()
}

動かない

ここで Add Top ボタンを押すとデータが追加されるときにアニメーションが発生しません。

こんな感じに case 内のViewにidをつけても同じです。

  List(viewModel.values) { v in
            // Listの直下でswitch caseするとアニメーションしなくなる
            switch v {
            case let .int(value):
                Text("Int: \(value)")
                    .id(v.id)
            case let .float(value):
                Text("Float: \(value)")
                    .id(v.id)
            }
        }

解決方法

こんな感じにList直下にswitch caseを入れずに一つのViewで囲みました(Groupでは駄目でした).

 var body: some View {
        List(viewModel.values) { v in
            VStack {
                switch v {
                case let .int(value):
                    Text("Int: \(value)")
                case let .float(value):
                    Text("Float: \(value)")
                }
            }
        }
        .listStyle(.plain)
        .safeAreaInset(edge: .bottom) {
            Button("Add Top") {
                withAnimation {
                    viewModel.insert()
                }
            }
        }
    }

するとこんなふうにちゃんとアニメーションするようになりました。

原因

原因はよくわかりません...

なんとなくList直下でswitch case したらViewの識別ができなくなって追加されたviewがわからなくなって reloadDataしたような動作になってしまうんだろうなと考えています...

UIKitと違って細かい挙動を推測しにくいのがSwiftUIの難しいところですね...