Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unwanted animation when moving items in SwiftUI list

I have a SwiftUI List like in the example code below.

struct ContentView: View {
    @State var numbers = ["1", "2", "3"]
    @State var editMode = EditMode.inactive

    var body: some View {
        NavigationView {
            List {
                ForEach(numbers, id: \.self) { number in
                    Text(number)
                }
                .onMove {
                    self.numbers.move(fromOffsets: $0, toOffset: $1)
                }
            }
            .navigationBarItems(trailing: EditButton())
        }
    }
}

When I enter edit mode and move the item one position up the strange animation happens after I drop the item (see the gif below). It looks like the dragged item comes back to its original position and then moves to the destination again (with animation)

strange animation when reordering items

What's interesting it doesn't happen if you drag the item down the list or more than one position up.

I guess it's because the List performs animation when the items in the state get reordered even though they were already reordered on the view side by drag and drop. But apparently it handles it well in all the cases other than moving item one position up.

Any ideas on how to solve this issue? Or maybe it's a known bug?

I'm using XCode 11.4.1 and the build target is iOS 13.4

(Please also note that in the "real world" app I'm using Core Data and when moving items their order is updated in the DB and then the state is updated, but the problem with the animation looks exactly the same.)

like image 780
Mateusz K Avatar asked May 03 '20 08:05

Mateusz K


People also ask

How to stop animation in SwiftUI?

You can no longer disable animations in SwiftUI using the deprecated animation(nil) modifier. We can use the transaction modifier to create the same result and disable animations for a specific view in SwiftUI.

What is SwiftUI animation?

SwiftUI includes basic animations with predefined or custom easing, as well as spring and fluid animations. You can adjust an animation's speed, set a delay before an animation starts, or specify that an animation repeats.

Does SwiftUI use Core Animation?

SwiftUI uses Core Animation for rendering by default, and its performance is great for most animations. But if you find yourself creating a very complex animation that seems to suffer from lower framerates, you may want to utilize the power of Metal, which is Apple's framework used for working directly with the GPU.


2 Answers

Here is solution (tested with Xcode 11.4 / iOS 13.4)

var body: some View {
    NavigationView {
        List {
            ForEach(numbers, id: \.self) { number in
                HStack {
                    Text(number)
                }.id(UUID())        // << here !!
            }
            .onMove {
                self.numbers.move(fromOffsets: $0, toOffset: $1)
            }
        }
        .navigationBarItems(trailing: EditButton())
    }
}
like image 199
Asperi Avatar answered Oct 18 '22 09:10

Asperi


Here's a solution based on Mateusz K's comment in the accepted answer. I combined the hashing of order and number. I'm using a complex object in place of number which gets dynamically updated. This way ensures the list item refreshes if the underlying object changes.

class HashNumber : Hashable{
        
        var order : Int
        var number : String
        
        init(_ order: Int, _ number:String){
            self.order = order
            self.number = number
        }
        
        static func == (lhs: HashNumber, rhs: HashNumber) -> Bool {
            return lhs.number == rhs.number && lhs.order == rhs.order
        }
        //
        func hash(into hasher: inout Hasher) {
            hasher.combine(number)
            hasher.combine(order)
        }
    }
    
    func createHashList(_ input : [String]) -> [HashNumber]{
        
        var r : [HashNumber] = []
        
        var order = 0
        for i in input{
            let h = HashNumber(order, i)
            r.append(h)
            order += 1
        }
        
        return r
    }
    
    struct ContentView: View {
        @State var numbers = ["1", "2", "3"]
        @State var editMode = EditMode.inactive
        
        var body: some View {
            NavigationView {
                List {
                    ForEach(createHashList(numbers), id: \.self) { number in
                        Text(number.number)
                    }
                    .onMove {
                        self.numbers.move(fromOffsets: $0, toOffset: $1)
                    }
                }
                .navigationBarItems(trailing: EditButton())
            }
        }
    }
like image 24
Jerry Sha Avatar answered Oct 18 '22 07:10

Jerry Sha