App crashes after deleting list item from CoreData - no debug output

My iOS 15.5 SwiftUI app crashes on iPhone 11 simulator. After reloading the app the CoreData entities have been successfully deleted. The crash occurs sometime between the last executable line of code in a delete confirmation alert which reloads the list of items in the view without error. Execution never reaches the body property of the view.

I can't think of any other way to determine what is causing the error and I cannot decode the crash report to help me! Any ideas would be much appreciated as the app is otherwise working great!!

Here is the crash report:


    

    @ObservedObject var vm: CourseListViewModel

    private var isPreview: Bool

    

    @State private var refreshDataOnReappear = false

    @State private var showDeleteConfirmationAlert = false

    @State private var showDeletionFailedAlert = false

    @State private var coursesToBeDeleted: [Course] = []

    

    init(isPreview: Bool = false) {

        self.isPreview = isPreview

        self.vm = CourseListViewModel(manager: isPreview ? PersistenceManager.preview : PersistenceManager.shared)

    }

    

    var body: some View {

        List {

            ForEach(vm.allCourses) { course in

                NavigationLink(destination: CourseEditView(isPreview: isPreview, course: course)) {

                    courseListItem(course)

                }

            }

            .onDelete { indices in

                coursesToBeDeleted = indices.map{ index in

                    vm.allCourses[index]

                }

                showDeleteConfirmationAlert.toggle()

            }

        }

        .navigationTitle("Golf courses")

        .toolbar {

            ToolbarItem {

                NavigationLink {

                    CourseEditView(isPreview: isPreview)

                } label: {

                    Label("Add course", systemImage: "plus")

                        .labelStyle(.titleAndIcon)

                }

            }

        }

        .onAppear {

            if refreshDataOnReappear {

                vm.refresh()

                refreshDataOnReappear = false

            }

        }

        .onDisappear() {

            refreshDataOnReappear = true

        }

        .alert("Delete Course", isPresented: $showDeleteConfirmationAlert) {

            Button(role: .destructive) {

                if !vm.deleteCourses(courses: coursesToBeDeleted) {

                    withAnimation {

                        showDeletionFailedAlert.toggle()

                    }

                } else {

                    vm.refresh()

                }

                coursesToBeDeleted.removeAll()

            } label: {

                Text("Delete")

            }

            Button("Cancel", role: .cancel) { }

        } message: {

            Text("Deleting a course will delete all cards and stored rounds on this course! Are you sure you want to continue?")

        }

        .alert("Deletion Failed", isPresented: $showDeletionFailedAlert) {

            Button("OK") {}

        } message: {

            Text("Attempt to delete the selected course failed. Restarting the app and trying again may resolve the issue.")

        }



    }

    

    func courseListItem(_ course: Course) -> some View {

        VStack(alignment: .leading, spacing: 4) {

            Text(course.name)

            if course.courseCards.count > 0 {

                HStack(alignment: .top) {

                    Text("\(course.courseCards[0].cardHoles.count) holes ")

                    VStack(alignment: .leading, spacing: 4) {

                        ForEach(cardsByLength(course: course)) { card in

                            HStack {

                                Image(systemName: "menucard").foregroundColor(Color(uiColor: card.uiColor))

                                Text("\(card.teeColour): \(card.length) yards, par \(card.par)")

                            }

                            

                        }

                    }

                }

                .padding(.leading)

                .font(.caption)

            } else {

                Text("No cards entered yet!").font(.caption)

            }

        }

        .padding(.vertical, 8)

    }

    

    func cardsByLength(course: Course) -> [Card] {

        course.courseCards.sorted(by: { $0.length > $1.length })

    }

}
Answered by DelawareMathGuy in 727502022

hi @ex-

thanks for taking the time to post some extended code.

three quick observations ...

for CardEditView, it's interesting that such is created in an expression NavigationLink(destination: CourseEditView(isPreview: isPreview, course: course)), and by using an init() method, the CardEditView then builds its view model (which, by the way, i think you want to be a @StateObject, not an @ObservedObject). it appears that code allows for the optional properties of the course to be nil-coalesced into meaningful values.

in contrast, for CardDetailsView, you're using an alternative syntax for creation inside a NavLink via CardDetailsView(viewModel: CardDetailsViewModel(manager: vm.persistenceManager, course: vm.course!)), which puts the view model into the view as an argument, rather than putting the course into the view as an argument and letting the view create its own view model. but here you're explicitly force-unwrapping the course? (and you mentioned this in the comment above.)

we don't see and code for CardDetailsView and we don't know exactly how that works with the course through its own view model; and the same is true for CardLinkView that accepts an incoming course argument.

as for recommendations in tracking down the problem,

  • i'd certainly be suspicious of the force-unwrapped reference, but that may simply be a red herring (indeed, i think you'd first want to look at using an explicit init() for CardDetailsView that accepts an incoming course and that creates its own view model).
  • the basic problem with core data deletions and SwiftUI is that SwiftUI may indeed try to access views that reference the deleted object ... but your views often expect to be accessed only with a valid object. in fact, the object may still exist for a short time (it is not itself nil), but it is a faulted Core Data object with all of its fields zeroed-out ... so its values are zero for any integer, false for any boolean, and nil for any optional. my fall-back: nil-coalesce everything.
  • your original posting contained the expression Text("\(course.courseCards[0].cardHoles.count) holes "). if course is on its way to core data deletion, one wonders what the [0] array reference means.
  • you might not need a full implementation of view models in your app. a simpler approach of referencing the incoming course argument as an @ObservedObject, properly nil-coalescing the properties of the course within the view, and using a number of functions within the view (rather than putting those in a separate view model) might be considered.

finally, as long as we're talking view models, you may want CourseEditViewModel to subscribe to change notifications that come from its associated course and to relay them as a change of the view model itself to the associated view. this will save you a little grief in having to manually call the refresh() method when you change properties in a course. you can use Combine to do this (at least i think this is it):

// in `CourseEditViewModel`
import Combine
var cancellables = Set<AnyCancellable>()
course.objectWillChange.sink({ _ in self.objectWillChange.send() }).store(in: &cancellables)

so, sorry, i may not have a silver bullet for you on this one, but i hope you'll find something useful above.

DMG

Further to my post above, I tried a simpler view to test the delete logic which worked fine with no errors. This would suggest that the problem is somewhere in the original view coding.

import SwiftUI

struct CourseListForDeleteTestView: View {
    
    @ObservedObject var vm: CourseListViewModel
    @State private var showDeleteConfirmationAlert = false
    
    init(isPreview: Bool = false) {
        self.vm = CourseListViewModel(manager: isPreview ? PersistenceManager.preview : PersistenceManager.shared)
    }
    
    var body: some View {
        VStack {
            List {
                ForEach(vm.allCourses) { course in
                    Text(course.name)
                }
                .onDelete { indices in
                    if let idx = indices.first {
                        if vm.deleteCourses(courses: [vm.allCourses[idx]]) {
                            showDeleteConfirmationAlert.toggle()
                            vm.refresh()
                        }
                    }
                }
            }
        }
        .alert("Course deleted", isPresented: $showDeleteConfirmationAlert) {
            Button(role: .cancel) {
            } label: {
                Text("OK")
            }
        }
    }
}

I’m having the same problem in Swift Playgrounds for iPad (and I’m not even using CoreData). I’ve put in a bunch of debugging to make sure no indexes are out of bounds, but I’m guessing something is being cached related to the ForEach loop where it’s either referencing the deleted item or using a cached version of the array and not noticing the array has been modified.

Note that the debug print statement does not seem to execute before using the index and the list is being edited elsewhere so I think there could be some sort of observer/synchronization issue going on…

struct HeartsScoreBox: View {
    @ObservedObject var round: HeartsRound
    @State var playerIndex: Int
    var body: some View {
        VStack {
            let _ = print("HSB# scores: \(round.playerScores.count), index: \(playerIndex)")
            let score = round.playerScores[playerIndex]
            Text(String(describing:score))
        }
    }
}

And the crash report:

Application Specific Information:
dyld: dyld4 config: DYLD_INSERT_LIBRARIES=/System/Library/PrivateFrameworks/PreviewsInjection.framework/PreviewsInjection
libswiftCore.dylib: Swift/ContiguousArrayBuffer.swift:575: Fatal error: Index out of range

Thread:
0      libswiftCore.dylib                        closure #1 in closure #1 in closure #1 in _assertionFailure(_:_:file:line:flags:)
1      libswiftCore.dylib                        closure #1 in closure #1 in closure #1 in _assertionFailure(_:_:file:line:flags:)
2      libswiftCore.dylib                        closure #1 in closure #1 in _assertionFailure(_:_:file:line:flags:)
3      libswiftCore.dylib                        closure #1 in _assertionFailure(_:_:file:line:flags:)
4      libswiftCore.dylib                        _assertionFailure(_:_:file:line:flags:)
5      libswiftCore.dylib                        _ArrayBuffer._checkInoutAndNativeTypeCheckedBounds(_:wasNativeTypeChecked:)
6      libswiftCore.dylib                        Array.subscript.getter
7      Previews_HeartsGameView_Thunk_1.dylib     closure #1 in HeartsScoreBox.__preview__body.getter /private/var/mobile/Library/Mobile Documents/iCloud~com~apple~Playgrounds/Documents/Score!.swiftpm/Games/Hearts/HeartsGameView.swift:11
8      SwiftUI                                   VStack.init(alignment:spacing:content:)
9      Previews_HeartsGameView_Thunk_1.dylib     HeartsScoreBox.__preview__body.getter /private/var/mobile/Library/Mobile Documents/iCloud~com~apple~Playgrounds/Documents/Score!.swiftpm/Games/Hearts/HeartsGameView.swift:9
10     Score!                                    protocol witness for View.body.getter in conformance HeartsScoreBox /<compiler-generated>:0
11     SwiftUI                                   partial apply for closure #1 in ViewBodyAccessor.updateBody(of:changed:)
12     SwiftUI                                   closure #1 in BodyAccessor.setBody(_:)
13     SwiftUI                                   ViewBodyAccessor.updateBody(of:changed:)
14     SwiftUI                                   DynamicBody.updateValue()
15     SwiftUI                                   partial apply for implicit closure #2 in implicit closure #1 in closure #1 in closure #1 in Attribute.init<A>(_:)
16     AttributeGraph                            AG::Graph::UpdateStack::update()
17     AttributeGraph                            AG::Graph::update_attribute(AG::data::ptr<AG::Node>, unsigned int)
18     AttributeGraph                            AG::Graph::value_ref(AG::AttributeID, AGSwiftMetadata const*, unsigned char&)
19     AttributeGraph                            AGGraphGetValue
20     SwiftUI                                   GraphHost.updatePreferences()
21     SwiftUI                                   ViewGraph.updateOutputs()
22     SwiftUI                                   closure #1 in ViewRendererHost.render(interval:updateDisplayList:)
23     SwiftUI                                   ViewRendererHost.render(interval:updateDisplayList:)
24     SwiftUI                                   _UIHostingView.layoutSubviews()
25     SwiftUI                                   @objc _UIHostingView.layoutSubviews()
26     UIKitCore                                 -[UIView(CALayerDelegate) layoutSublayersOfLayer:]
27     QuartzCore                                CA::Layer::layout_if_needed(CA::Transaction*)
28     UIKitCore                                 -[UIView(Hierarchy) layoutBelowIfNeeded]
29     SwiftUI                                   closure #3 in performUpdates #1 <A, B>() in UITableViewListCoordinator.updateUITableView(_:to:transaction:)
30     SwiftUI                                   thunk for @callee_guaranteed () -> ()
31     SwiftUI                                   thunk for @escaping @callee_guaranteed () -> ()
32     UIKitCore                                 +[UIView(Animation) performWithoutAnimation:]
33     SwiftUI                                   performUpdates #1 <A, B>() in UITableViewListCoordinator.updateUITableView(_:to:transaction:)
34     SwiftUI                                   thunk for @callee_guaranteed () -> ()
35     SwiftUI                                   thunk for @escaping @callee_guaranteed () -> ()
36     UIKitCore                                 +[UIView(Animation) performWithoutAnimation:]
37     SwiftUI                                   closure #2 in UITableViewListCoordinator.updateUITableView(_:to:transaction:)
38     SwiftUI                                   thunk for @escaping @callee_guaranteed () -> ()
39     SwiftUI                                   static Update.end()
40     SwiftUI                                   static NSRunLoop.flushObservers()
41     SwiftUI                                   closure #1 in closure #1 in static NSRunLoop.addObserver(_:)
42     SwiftUI                                   specialized thunk for @callee_guaranteed () -> (@error @owned Error)
43     libswiftObjectiveC.dylib                  autoreleasepool<A>(invoking:)
44     SwiftUI                                   closure #1 in static NSRunLoop.addObserver(_:)
45     SwiftUI                                   @objc closure #1 in static NSRunLoop.addObserver(_:)
46     CoreFoundation                            __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
…

Found a fix! (Credit: https://blog.apptekstudios.com/2020/05/quick-tip-avoid-crash-when-using-foreach-bindings-in-swiftui/)

Add this extension and then when referencing the index, use [safe:index] and everything works as expected!

// Fix for race condition issue
extension Binding where Value: MutableCollection {
    subscript(safe index: Value.Index) -> Binding<Value.Element> {
        // Get the value of the element when we first create the binding
        // Thus we have a 'placeholder-value' if `get` is called when the index no longer exists
        let safety = wrappedValue[index]
        return Binding<Value.Element>(
            get: {
                guard self.wrappedValue.indices.contains(index)
                else { return safety } //If this index no longer exists, return a dummy value
                return self.wrappedValue[index]
            },
            set: { newValue in
                guard self.wrappedValue.indices.contains(index)
                else { return } //If this index no longer exists, do nothing
                self.wrappedValue[index] = newValue
            })
    }
}

Further code posted in response to @DelawareMathGuy's comment on my original post:

struct CourseEditView: View {
  @Environment(\.colorScheme) var colorScheme
  var backgroundColor: Color {
    Color(uiColor: (colorScheme == .dark ? .systemBackground : .secondarySystemBackground))
  }
   
  enum Field: Hashable {
    case name
  }
   
  @ObservedObject var vm: CourseEditViewModel
  @FocusState private var nameFieldFocused: Field?
   
  init(isPreview: Bool, course: Course? = nil) {
    self.vm = CourseEditViewModel(manager: isPreview ? PersistenceManager.preview : PersistenceManager.shared, course: course)
     
  }
   
  var body: some View {
    VStack {
      Form {
        HStack {
          LabelledTextField(value: $vm.name, label: "Course name")
            .focused($nameFieldFocused, equals: .name)
          if !vm.isValid {
            Image(systemName: "exclamationmark.triangle.fill").foregroundColor(.yellow)
          }
        }
        Section {
          List {
            ForEach(vm.cards, id: \.teeColour) { card in
              NavigationLink(destination: {
                CardDetailsView(viewModel: CardDetailsViewModel(manager: vm.persistenceManager, course: vm.course!, card: card))
              }, label: {
                CardLinkView(card: card)
              })
            }
            //          .onDelete(perform: deleteCards)
          }
        } header: {
          HStack {
            if vm.isValid {
              Text("CARDS")
              Spacer()
              NavigationLink {
                CardDetailsView(viewModel: CardDetailsViewModel(manager: vm.persistenceManager, course: vm.course!))
              } label: {
                HStack(spacing: 4) {
                  Image(systemName: "plus")
                    .font(.title2)
                  Text("Add card")
                }
                .foregroundColor(.accentColor)
              }
            }
          }
        }
      }
      .padding(.top)
      .navigationTitle("Add/edit Course")
      .onAppear {
        if !vm.isValid {
          DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            nameFieldFocused = .name
          }
        }
        vm.refresh()
      }
    }
    .background(backgroundColor)
  }
}
class CourseEditViewModel: ObservableObject {
   
  var course: Course?
   
  @Published var name: String = "" {
    didSet {
      isValid = !name.isEmpty
      if isValid {
        saveChanges()
      }
    }
  }
  @Published var isValid: Bool = false
  var cards: [Card] {
    course?.courseCards.sorted(by: { $0.teeColourOrder < $1.teeColourOrder }) ?? []
  }
  private (set) var persistenceManager: PersistenceManager
  private var isInitialising: Bool = true
   
  init(manager: PersistenceManager, course: Course? = nil) {
    self.persistenceManager = manager
    if course != nil {
      self.course = course
      self.name = course?.name ?? "Unknown"
    }
    isInitialising = false
  }
   
  func refresh() {
    objectWillChange.send()
  }
   
  func saveChanges() {
    guard !isInitialising else { return }
    var saved = false
    if let course = self.course {
      course.name = name
      saved = persistenceManager.applyChanges()
    } else {
      let course = persistenceManager.courseRepository.new { course in
        course.id = UUID()
        course.name = self.name
      }
      saved = persistenceManager.applyChanges()
      if saved {
        self.course = course
      }
    }
    if !saved {
      persistenceManager.rollback()
    }
  }
}
Accepted Answer

hi @ex-

thanks for taking the time to post some extended code.

three quick observations ...

for CardEditView, it's interesting that such is created in an expression NavigationLink(destination: CourseEditView(isPreview: isPreview, course: course)), and by using an init() method, the CardEditView then builds its view model (which, by the way, i think you want to be a @StateObject, not an @ObservedObject). it appears that code allows for the optional properties of the course to be nil-coalesced into meaningful values.

in contrast, for CardDetailsView, you're using an alternative syntax for creation inside a NavLink via CardDetailsView(viewModel: CardDetailsViewModel(manager: vm.persistenceManager, course: vm.course!)), which puts the view model into the view as an argument, rather than putting the course into the view as an argument and letting the view create its own view model. but here you're explicitly force-unwrapping the course? (and you mentioned this in the comment above.)

we don't see and code for CardDetailsView and we don't know exactly how that works with the course through its own view model; and the same is true for CardLinkView that accepts an incoming course argument.

as for recommendations in tracking down the problem,

  • i'd certainly be suspicious of the force-unwrapped reference, but that may simply be a red herring (indeed, i think you'd first want to look at using an explicit init() for CardDetailsView that accepts an incoming course and that creates its own view model).
  • the basic problem with core data deletions and SwiftUI is that SwiftUI may indeed try to access views that reference the deleted object ... but your views often expect to be accessed only with a valid object. in fact, the object may still exist for a short time (it is not itself nil), but it is a faulted Core Data object with all of its fields zeroed-out ... so its values are zero for any integer, false for any boolean, and nil for any optional. my fall-back: nil-coalesce everything.
  • your original posting contained the expression Text("\(course.courseCards[0].cardHoles.count) holes "). if course is on its way to core data deletion, one wonders what the [0] array reference means.
  • you might not need a full implementation of view models in your app. a simpler approach of referencing the incoming course argument as an @ObservedObject, properly nil-coalescing the properties of the course within the view, and using a number of functions within the view (rather than putting those in a separate view model) might be considered.

finally, as long as we're talking view models, you may want CourseEditViewModel to subscribe to change notifications that come from its associated course and to relay them as a change of the view model itself to the associated view. this will save you a little grief in having to manually call the refresh() method when you change properties in a course. you can use Combine to do this (at least i think this is it):

// in `CourseEditViewModel`
import Combine
var cancellables = Set<AnyCancellable>()
course.objectWillChange.sink({ _ in self.objectWillChange.send() }).store(in: &cancellables)

so, sorry, i may not have a silver bullet for you on this one, but i hope you'll find something useful above.

DMG

FIXED IT!!!

I changed the ForEach loop to iterate through the array indices rather than the array itself. The following code now works successfully (using iOS 16 version of NavigationStack/NavigationLink). I assume, but haven't tested, that the same modification in the iOS 15 version would also have worked.

     List {
      ForEach(0..<vm.allCourses.count, id: \.self) { idx in
        if let course = vm.allCourses[idx] {
          NavigationLink(value: course.id) {
            courseListItem(course)
          }
        }
      }
      .onDelete { indices in
        coursesToBeDeleted = indices.map{ index in
          vm.allCourses[index]
        }
        showDeleteConfirmationAlert.toggle()
      }
    }
    ...

Thanks @DelawareMathGuy for your contribution. :-D

Hello @ex-coder,

I actually just hit the same problem you were experiencing here. And the fix was actually really simple! Just have to add an Id to the List view. Here is an example: List { ForEach(localTemplates, id:\.templateId) { template in LocalTemplateListCellView( template: template, rating: Int(template.rating), share: $share, use: $use) } .onDelete { indexSet in confirmDelete = true deleteIndexSet = indexSet } .alert("Delete? This can not be undone...", isPresented: $confirmDelete) { Button("Yes", role: .destructive) { guard let indexSet = deleteIndexSet else { return } delete(at: indexSet) confirmDelete = false } Button("No", role: .cancel) { confirmDelete = false } } } .id(localTemplates.count)

Notice the ".id(localTemplates.count)". This generates a new ID for the list when it changes, and avoids the issue of anything being nil when the view refreshes after the array changes.

Hopefully this helps someone else in the future. :)

App crashes after deleting list item from CoreData - no debug output
 
 
Q