2

Focusing on comprehending the underlying concept of the various animation behaviors, I am interested of programmatically animate the change of a row in a List.

I am able to achieve it in the following example:

struct First: View {
  @State var items = ["One", "Two", "Three", "Four"]

  var body: some View {
    VStack {
        List(items, id: \.self) { item in
            Text(item)
        }
        
        Button("Hit me") {
            withAnimation {
                items.swapAt(2, 0)
            }
        }
    }
 }
}

As you can see upon tapping on the button, we receive a nice swapping animation in the cells. We can see that cells are actually moving, one to the top and the other downwards:

List: nice swapping animation in the cells

When I change the list code to the following:

List(items.indices, id: \.self) { index in
   Text(items[index])
}

As you can see the prior animation doesn't work anymore and instead I receive a text change animation:

List: animation doesn't work anymore and instead I receive a text change animation

Is there a solution to keep the first (default) swapping animation ?

Thanks.

OhadM
  • 4,687
  • 1
  • 47
  • 57
  • 1
    You should watch "Demystify SwiftUI" `\.self` and `indices` are "unsafe" when dealing with SwiftUI. `ForEach` requires an identifier because SwiftUI depends on it for reloading, updating, animating, efficiency, etc. – lorem ipsum May 11 '23 at 15:28

2 Answers2

1

It's a question of identity. SwiftUI keeps track of view elements by their identity, and it can only animate between states when the identities of the single elements stay the same.

In the first code, the List id is of type String, the string changes place but keeps the same id.

In the second code the List id is of type Index, but when swapping the index does not change, only the relative string at that index.

So in result 1. animates the position based on string id, and 2. animates the string based on index id.

BUT you can tell SwiftUI explicitly which id to use, also in the second code, like this:

            List(items.indices, id: \.self) { index in
                Text(items[index]).id(items[index]) // Here
            }
ChrisR
  • 9,523
  • 1
  • 8
  • 26
1

You can't use indices with the ForEach, it's a View not a for loop. The first problem is the animations as you saw, the next problem is it will crash when you remove items.

When your list items are an array of value types in an @State you need to pass a valid id param, e.g.

List(items, id: \.uniqueIdentifier) { item in

The reason is so the List can track the inserts, removes and deletes. Value types don't have inherent identity like objects do because they are copied everywhere rather than a reference being copied.

Most SwiftUI Views that work on arrays like List make use of the Identifiable protocol to make this simpler, e.g.

struct Item: Identifiable {
    let id = UUID() // this could also be a computed var that concatenates a bunch of properties that result in a unique string for this Item.

    let text: String
}

struct First: View {
  @State var items = [Item(text: "One"), Item(text: "Two"), Item(text: "Three"), Item(text: "Four")]

  var body: some View {
    VStack {
        List(items) { item in // no id param needed when the item conforms to Identifiable
            Text(item.text)
        }
    ...
malhal
  • 26,330
  • 7
  • 115
  • 133
  • Thanks but you didn't answer the question – OhadM May 11 '23 at 13:22
  • Sorry it wasn't clear. Take another look at the first sentence please, thanks. – malhal May 11 '23 at 14:23
  • wdyt about the accepted answer ? – OhadM May 11 '23 at 14:43
  • That can crash if you remove an item from the array, many have had that problem, e.g. https://stackoverflow.com/questions/59335797/swiftui-foreach-using-array-index-crashes-when-rows-are-deleted – malhal May 11 '23 at 14:46
  • You mean if I'll use the offset/index from the indices ? – OhadM May 11 '23 at 15:11
  • Yes if you do `items[index]` inside the `List` it'll crash – malhal May 11 '23 at 20:28
  • 1
    I totally agree with @malhal. You shouldn't be using the index version for the mentioned reasons. I focussed on explaining the concept behind the different animation behaviours, as I understood this to be the focus of the question. – ChrisR May 12 '23 at 00:00
  • Adding to the identity problem: if you use pure strings with `id: \.self` and two strings have the same content, their id would be identical which leads to a number of issues. – ChrisR May 12 '23 at 00:03