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)
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.)
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.
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.
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.
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())
}
}
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())
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With