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 })
}
}
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 ")
. ifcourse
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