🧐

SwiftUI: swipeActionsが思った通りにアニメーションしない時

2024/09/17に公開

SwiftUIのListに対してswipeActionsを書いた時に期待通りアニメーションしないパターン2つと遭遇しました。
現象とその解決策を書いておきます。

結論を先に書くと、公式に従って書いていれば問題ない話ではあります。

https://developer.apple.com/documentation/swiftui/view/swipeactions(edge:allowsfullswipe:content:)#discussion

https://developer.apple.com/documentation/swiftui/list/#overview

アニメーションが意図通り動かないパターンは

  1. アイテム削除時にアニメーションされない
  2. アイテム更新時に変なアニメーションをする

でした。
問題が発生するコードを提示し、その下に修正版のコードといった順で記載していますので腕に自信のある方は、どこを修正すれば改修できるのか考えながら読んで楽しんでいただければと思います。

1.アイテム削除時にアニメーションされない

こんな感じにリストの削除した時にアニメーションが発生しないです。
以下がそのコードです。このコードの中にあるダメなところは何でしょうか?

struct SampleData: Identifiable {
    let id = UUID()
}

struct SampleCell: View {
    var data: SampleData
    var body: some View {
        Text(data.id.uuidString)
    }
}

struct Sample: View {
    @State var list: [SampleData] = [
        .init(),
        .init(),
        .init(),
        .init()
    ]

    func remove(uuid: UUID) {
        if let index = list.firstIndex(where: {$0.id == uuid}) {
            list.remove(at: index)
        }
    }

    var body: some View {
        List(list) { data in
            SampleCell(data: data)
                .swipeActions(edge: .trailing, allowsFullSwipe: true) {
                    Button {
                        remove(uuid: data.id)
                    } label: {
                        Image (systemName: "trash.fill")
                    }.tint(.red)
                }
        }
    }
}    

修正版

  struct Sample: View {
        List(list) { data in
             SampleCell(data: data)
                 .swipeActions(edge: .trailing, allowsFullSwipe: true) {
-                    Button {
+                    Button(role: .destructive) {
                         remove(uuid: data.id)
                     } label: {
                         Image (systemName: "trash.fill")
-                    }.tint(.red)
+                    }
                 }
         }
     }

正解は、削除は破壊的な変更なのでちゃんとrole: .destructiveをつけましょうって話でした。言われてみると当たり前な気はするけどボタンのロールとアニメーションに繋がりがあるとは想像が難しかったです。
公式の1番最初のところに書いてあるので困ったら公式をちゃんと読むの大事。
https://developer.apple.com/documentation/swiftui/view/swipeactions(edge:allowsfullswipe:content:)#discussion

2. アイテム更新時に変なアニメーションをする


今度はスワイプでフラグをつけるような例になります。
アニメーションはするのですが、想像していたアニメーションの期待値とは少し変わったアニメーションになります。
以下がそのコードです。このコードの中にあるダメなところは何でしょうか?

struct SampleData: Identifiable, Hashable {
    let id = UUID()
    var flag = true
}

struct SampleCell: View {
    var data: SampleData
    var body: some View {
        HStack {
            Text(data.id.uuidString)
            if data.flag {
                Image(systemName: "flag.fill")
                    .font(.caption)
                    .foregroundColor (.orange)
            }
        }
    }
}

struct Sample: View {
    @State var list: [SampleData] = [
        .init(flag: true),
        .init(flag: true),
        .init(flag: false),
        .init(flag: false)
    ]

    func toggleFlag(uuid: UUID) {
        if let index = list.firstIndex(where: {$0.id == uuid}) {
            list[index].flag.toggle()
        }
    }

    var body: some View {
        List(list, id: \.self) { data in
            SampleCell(data: data)
                .swipeActions(edge: .trailing, allowsFullSwipe: true) {
                    Button {
                        toggleFlag(uuid: data.id)
                    } label: {
                        if data.flag {
                            Image (systemName: "flag.slash.fill")
                        } else {
                            Image (systemName: "flag.fill")
                        }
                    }.tint(.orange)
                }
        }
    }
}

修正版

-struct SampleData: Identifiable, Hashable {
+struct SampleData: Identifiable {
     let id = UUID()
     var flag = true
 }

 struct Sample: View {
         }
     }
     var body: some View {
-        List(list, id: \.self) { data in
+        List(list, id: \.self.id) { data in
             SampleCell(data: data)
                 .swipeActions(edge: .trailing, allowsFullSwipe: true) {
                     Button {
~

もしくは

-struct SampleData: Identifiable, Hashable {
+struct SampleData: Identifiable {
     let id = UUID()
     var flag = true
 }

 struct Sample: View {
         }
     }
     var body: some View {
-        List(list, id: \.self) { data in
+        List(list) { data in
             SampleCell(data: data)
                 .swipeActions(edge: .trailing, allowsFullSwipe: true) {
                     Button {

\.selfHashableです。SampleDataの全てのプロパティの変更でListの再描画が走る可能性が高く、アニメーションが意図しないタイミングで発生してしまいます。

結論だけ見ると初歩的な内容ではあるのですが、この現象に遭遇した際、見当違いをして全く関係ないところを調査して時間を使ってしまったので記事にしました。

現場からは以上です!

Discussion