My goal is to create a list like this in SwiftUI
Every time the user taps add, a new row is created and then on swiping to delete, the respective row needs to be deleted.
In order to do that, I start with a very basic model
struct SnagItem: Codable {
private(set) var reference: Int = 0
private(set) var details: String = ""
private(set) var isCompleted: Bool = false
enum CodingKeys: String, CodingKey, CaseIterable {
case reference = "Reference"
case details = "Details"
case isCompleted = "IsCompleted"
}
}
extension SnagItem: Hashable { }
I use this model within a view model which I wish to bind to my SwiftUI view, this is the view model:
class SnagItemViewModel: ObservableObject {
private let snagItem: SnagItem
@Published var details: String = ""
@Published var isCompleted: Bool = false
init(withSnagItem snagItem: SnagItem) {
self.snagItem = snagItem
details = snagItem.details
isCompleted = snagItem.isCompleted
}
}
I have another view model that keeps track of all the snag items (rows) using an array. This also handles the adding and deletion of items.
class SnagItemsViewModel: ObservableObject {
@Published var snagItems: [SnagItemViewModel] = [SnagItemViewModel(withSnagItem: SnagItem())]
func getSnagItem(at index: Int) -> SnagItemViewModel {
snagItems[index]
}
func addSnagItem() {
snagItems.append(SnagItemViewModel(withSnagItem: SnagItem()))
}
func deleteSnagItemRecord(at indexSet: IndexSet) {
guard let index = indexSet.first else { return }
snagItems.remove(at: index)
}
}
@Published var snagItems: [SnagItemViewModel] = [SnagItemViewModel(withSnagItem: SnagItem())]
I've created a SwiftUI view that acts as a container:
struct SnagItemsView: View {
@StateObject var snagItemsViewModel: SnagItemsViewModel
var body: some View {
ForEach(0 ..< snagItemsViewModel.snagItems.count, id: \.self) { snagItemIndex in
let snagItemViewModel = snagItemsViewModel.getSnagItem(at: snagItemIndex)
SnagItemRowView(reference: snagItemIndex + 1,
snagItemViewModel: snagItemViewModel)
}
.onDelete(perform: delete(at:))
}
func delete(at indexSet: IndexSet) {
withAnimation {
snagItemsViewModel.deleteSnagItemRecord(at: indexSet)
}
}
}
Each row then has it's own view:
struct SnagItemRowView: View {
@State var reference: Int
@StateObject var snagItemViewModel: SnagItemViewModel
let gridItemColumnConfig = [
GridItem(.fixed(Constants.Frame.minGridItemWidth)),
GridItem(.flexible()),
GridItem(.fixed(Constants.Frame.minGridItemWidth))
]
var body: some View {
LazyVGrid(columns: gridItemColumnConfig, spacing: 8) {
ForEach(0 ..< SnagItem.CodingKeys.allCases.count, id: \.self) { columnIndex in
let snagItemCategory = SnagItem.CodingKeys.allCases[columnIndex]
if snagItemCategory == .reference {
Text("\(reference)")
} else if snagItemCategory == .details {
TextField(snagItemViewModel.localizedText(for: .enterValue),
text: $snagItemViewModel.details,
axis: .vertical)
} else {
Toggle("", isOn: $snagItemViewModel.isCompleted)
.labelsHidden()
}
}
}
}
}
Everything seems to work fine for the most part.
It starts off like this:
When the user taps Add, another row is added and doing this multiple times adds more rows:
The issues begin, when I try to delete:
As you can see I'm trying to delete row 3, after deletion this is the result is that row 4 gets deleted instead, atleast from a UI perspective:
I printed out the values from my get function and it seems that the view model actually seems to have the correct objects, the SwiftUI View doesn't seem to render the correct data however for some reason:
I added a breakpoint in my delete function, to see if the right index was being passed and it seems like everything looks good here:
Finally, when I try to add another row after the deletion process, the old data seems to be added back again:
And again, when I check the view model, the data seems to be right, but the SwiftUI view shows something else.
What am I doing wrong and how could I fix this ?