I am developing an iOS app using SwiftUI, and I’m experiencing graphical glitches when I use the indices of my state array in a ForEach loop instead of the elements themselves. Specifically, the UI elements in my list start to flicker and disappear when I attempt to reorder them.
The ‘Directions’ section is animated correctly, while the ‘Ingredients’ section appears glitchy.
Approximation of the ‘Add Recipe’ view
The only difference between these two sections is that one ForEach
iterates over the arrays indices, while the other iterates over the elements.
struct TestView: View {
@State private var testData = ["Test 1", "Test 2"]
var body: some View {
NavigationStack {
Form {
Section("Ingredients") {
ForEach(testData.indices, id: .self) {
Text(testData[$0])
}
.onMove {
testData.move(fromOffsets: $0, toOffset: $1)
}
}
Section("Directions") {
ForEach(testData, id: .self) {
Text($0)
}
.onMove {
testData.move(fromOffsets: $0, toOffset: $1)
}
}
}
}
}
}
What could be causing this issue and how can I fix it? Does anyone know the "why"? What are the SwiftUI mechanics behind this graphical hiccup?
2
Answers
ForEach
does not need your array’s indices for iteration. There is no reason to pass them into yourForEach
loop, especially since you are using.self
to identify the array items. The solution is clear from your question: Remove the.indices
portion of your code and use$0
instead oftestData[$0]
. It will run just fine, and you won’t have the graphical glitch.SwiftUI determines what Views to redraw and animate based on their identity. Ultimately, it creates a representation of the current hierarchy with IDs, then applies transformations, then compares the new representation with the old representation. It uses the changes to determine what to animate.
When you use indexes as the identity, you tie identity to current location, rather than to the value itself. So moving items around creates inconsistent situations.
Consider the list identified by unique values:
A B C D
. Applying the operation "move item 2 to position 0". This should result inC A B D
, and it does, and that is correct.But now consider the list identified by index:
0 1 2 3
. Now apply "move item 2 to position 0." The new list is still0 1 2 3
, since the identity is the current location, and changes when the item is moved. This breaks the precondition that the end-state should be consistent with the operations, and can glitch the UI. Whether it actually glitches the UI depends on the both the specific setup and implementation details of SwiftUI. It’s undefined behavior, so all kinds of things can happen.You very rarely should use indexes as identifiers, and if you must, then you cannot reorder the list.