I’m developing a SwiftUI app where I display a list of items, each containing a TextField. Since editing the textfield in one of these items causes all item views to re-render, I opt’d to implement equatable on the item views to improve performance.
However, after deleting an item from the list, the TextField bindings seem to get confused. Specifically, when I edit a TextField after a deletion, the text updates in multiple fields simultaneously. This can also cause the following error if it tries to update a textfield that no longer exists: Swift/ContiguousArrayBuffer.swift:675: Fatal error: Index out of range
Steps to reproduce the issue:
- Run the code provided below.
- Delete the item labeled "text 2" by tapping the "Delete" button next to it.
- Tap on the "text 3" TextField and start typing.
- Notice that both "text 3" and "text 4" TextFields update with the same text.
- Now delete "text 3"
- Now start typing in "text 4"
- Notice the app crashes with an index out of range error
struct MyDataItem: Identifiable, Equatable {
var id = UUID().uuidString
var text: String = ""
var placeholder: String = ""
static func == (lhs: MyDataItem, rhs: MyDataItem) -> Bool {
return lhs.id == rhs.id &&
lhs.text == rhs.text &&
lhs.placeholder == rhs.placeholder
}
}
struct BugPrepro: View {
@State var data: [MyDataItem] = [
MyDataItem(placeholder: "text 1"),
MyDataItem(placeholder: "text 2"),
MyDataItem(placeholder: "text 3"),
MyDataItem(placeholder: "text 4")
]
var body: some View {
VStack {
ForEach($data, id: .id) { $dataItem in
LineItemView(dataItem: $dataItem, onDelete: {
data.removeAll { $0.id == dataItem.id }
})
}
}
}
}
struct LineItemView: View, Equatable {
@Binding var dataItem: MyDataItem
var onDelete: () -> Void
var body: some View {
HStack {
TextField(dataItem.placeholder, text: $dataItem.text)
Button(action: onDelete) {
Text("Delete")
.foregroundColor(.red)
}
}
.padding()
.border(.black)
}
static func == (lhs: LineItemView, rhs: LineItemView) -> Bool {
return lhs.dataItem == rhs.dataItem
}
}
My understanding:
MyDataItem
conforms toIdentifiable
, so SwiftUI should track each item’s identity.- By implementing
Equatable
onLineItemView
, I’m trying to prevent unnecessary view updates to improve performance. - Despite this, deleting an item seems to cause the TextField bindings to overlap.
My question:
Why are the TextField
bindings getting mixed up after deleting an item when using Equatable on the view? How can I prevent the views from unnecessarily re-rendering without causing this issue?
2
Answers
Oh, wow, at first, I thought it was the old Objective-C issue where you couldn’t delete anything in a
forEach
loop, but it seems to be related to some internal SwiftUI optimization. I have played with it, and you can workaround the issue by moving@State var data
to@StateObject
Seems to me
ForEach
‘s automatic bindings feature is broken in this case of removing the lastTextField
and we have to wait for it to be fixed. In the meantime you can do it the old way:As for the performance issue of every
LineItemView
‘s body being called no matter which text is changed, I believe yourEquatable
implementation is correct – so basically you are trying to make it ignore theonDelete
closure which it can’t compare automatically.I submitted Feedback FB15573786 and linked here.