Search code examples
swiftuiindexoutofboundsexceptionswiftui-listswiftui-foreach

SwiftUI list ForEach index out of range when last array element deleted


I am learning SwiftUI at the moment and ran into this problem which I can't seem to fix. There might be a very simple solution. Essentially, I have a viewmodel which contains a list of NSManagedObjects (Room):

class RoomViewModel: ObservableObject, ViewModelProtocol {
    
    enum ViewState {
        case Loading
        case Normal
        case NoData
    }
    
    @Published var viewState: ViewState = .Loading
    @Published var rooms: [Room] = []
    
    // Get all the rooms
    func getRooms() {
        viewState = .Loading
        rooms = CoreDataManager.shared.getRooms()
        updateViewState()
    }
    
    // MARK: Update the view state
    func updateViewState() {
        viewState = rooms.count == 0 ? .NoData : .Normal
    }
}

On deletion of the last element from the rooms list in the viewmodel the foreach crashes:

if viewModel.rooms.count > 0 {
    List {
        ForEach(Array(viewModel.rooms.enumerated()), id: \.self.element.id) { index, room in
            RoomRow(room: self.viewModel.rooms[index], viewModel: viewModel, index: index)
        }.listRowBackground(Color.backgroundColor)
    }
} else {
    NoData()
}

When I delete a room this is the code that runs:

PersistenceController.shared.container.viewContext.delete(self.viewModel.rooms[selectedIndex])
self.viewModel.rooms.remove(at: selectedIndex)
PersistenceController.shared.saveContext()

The app then crashes on the ForEach loop in the view stating that the index was out of bounds. Is this some async problem, where the viewState updates before the element is deleted from the list in the viewmodel?


Solution

  • I found the solution to my problem. The problem appears to be that the ForEach is trying to look at the difference between the previous and the new data and is trying to grab deleted core data objects. I fixed the problem by changing the ForEach loop to check if the object we are looking at has been deleted

    ForEach(Array(viewModel.rooms.enumerated()), id: \.self.element.id) { index, room in
        if room.isFault {
            EmptyView()
        } else {
            RoomRow(room: self.viewModel.rooms[index], viewModel: viewModel, index: index)
        }
    }