'Crash when deleting an item from List in SwiftUI with custom RandomAccessCollection
Basic approach
I am currently tring to clean up my Core Data/SwiftUI code, and part of that is moving code out of my Views and into my ViewModels. Since it seems that @FetchRequest
does not work in VMs, I tried to create something similar that works in VMs (basically a wrapper around NSFetchedResultsController
):
class FetchedData<ResultType: NSManagedObject>: NSFetchedResultsController<NSFetchRequestResult>, NSFetchedResultsControllerDelegate, ObservableObject {
@Published var value: [ResultType] = []
convenience init(entity: ResultType.Type) {
let request = NSFetchRequest<NSFetchRequestResult>()
request.entity = ResultType.entity()
request.sortDescriptors = []
super.init(
fetchRequest: request,
managedObjectContext: PersistenceController.shared.context,
sectionNameKeyPath: nil,
cacheName: nil
)
self.delegate = self
do {
try self.performFetch()
value = (self.fetchedObjects as? [ResultType]) ?? []
} catch {
print("Could not perform fetch")
}
}
internal func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let newValue = controller.fetchedObjects as? [ResultType] else {
return
}
value = newValue
}
}
So far so good, I can use this as follows:
struct ContentView: View {
class ViewModel: ObservableObject {
private var cancellables: [AnyCancellable] = []
var data = FetchedData(entity: Element.self)
init() {
data.$value.sink { _ in
self.objectWillChange.send()
}
.store(in: &cancellables)
}
}
@StateObject var viewModel = ViewModel()
var body: some View {
List {
ForEach(viewModel.data.value, id: \.self) { element in
// show UI for element
}
}
}
}
Making FetchedData a collection
Now I wanted to simplify the usage of this a bit, and make FetchedData
a collection so I can use it directly and don't have to use value
. I added this:
extension FetchedData: RandomAccessCollection {
typealias Element = ResultType
typealias Index = Int
typealias Indices = Range<Int>
typealias Iterator = IndexingIterator<[Element]>
subscript(position: Int) -> ResultType {
return value[position]
}
var startIndex: Int {
return value.startIndex
}
var endIndex: Int {
return value.endIndex
}
__consuming func makeIterator() -> IndexingIterator<[Element]> {
return value.makeIterator()
}
}
and then changed my View to this:
struct ContentView: View {
class ViewModel: ObservableObject {
let data = LiveData(entity: Element.self)
}
@StateObject var viewModel = ViewModel()
var body: some View {
Group {
List {
ForEach(viewModel.data, id: \.self) { element in
// show UI for element
}
}
}
}
}
The Issue
It... kind of works. It will show and updated my data, and I can add Element
s and the view will update just fine. The problem occurs when I try to delete data using this:
.onDelete { indexSet in
indexSet.forEach { index in
let element = viewModel.data[index]
viewContext.delete(element)
}
try? viewContext.save()
}
My app will crash with a Fatal error: Index out of range
exception in the subscript
method of my extension.
For example, if I had 5 elements in my collection and delete one, first the deletion happens and then SwiftUI apparently queries index 4 from viewModel.data
(which doesn't exist anymore, since only 4 elements are left). The strange thing is that SwiftUI seems to query endIndex
before that, which returns the correct value, so I don't know why it would query something it knows is out of bounds. Also, it seems that for some reason, deleting the last element seems to work just fine.
I can't really make heads or tails of this. Since it works using viewModel.data.values
instead of just viewModel.data
I suppose there is something wrong in my extension FetchedData: RandomAccessCollection
, but I do not understand the issue here.
Any help would be appreciated!
Solution 1:[1]
Not sure if you still have this issue but I came across the same thing. The only way I could get it fixed was to use indices in the ForEach
ForEach(viewModel.data.indices, id: \.self) { idx in
// show UI for element
// using viewModel.data[idx] e.g.
Text(viewModel.data[idx].description)
}
Sadly, I am unable to explain why this works but relying on the underlying collection does not.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Brian Parkes |