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