As I struggled with developing my application I decided to learn some basics and created playground project using Core Data in SwiftUI, but I encountered problem I can't resolve and even root cause. Let me begin with describing what I tried to achieve and what the problem is.
I created 3 models:
ListEntity
- have name and to-many relationship to ListItemEntity
ListItemEntity
- has just flag and 2 to-one relationship to ListEntity
and NameEntity
NameEntity
- has just name and to-many relationship to ListItemEntity
For those models I created 3 basic views to show and add data. Views for showing and adding ListEntity
(first for list of them and second with their details) works fine without any problem. The problem I have is with 3rd view which consists of searchable list of NameEntity
(with add button, which just add new NameEntity
of name Name <counts of names>
). Each NameEntity
row in list is clickable and by clicking it you should add (or delete) ListItemEntity
to currently edited ListEntity
with selected NameEntity
. That works fine as long as you add only 1 item. When I try to add another I get error somewhere in context save method. The problem is, my save method is inside do { } catch { }
block and it should print error to console, but instead it crashes whole application.
Here is error I'm getting:
error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. -[NameEntity compare:]: unrecognized selector sent to instance 0x600002118ff0 with userInfo (null)
With call stack:
*** First throw call stack:
(
0 CoreFoundation 0x0000000180491128 __exceptionPreprocess + 172
1 libobjc.A.dylib 0x000000018008412c objc_exception_throw + 56
2 CoreFoundation 0x00000001804a5f78 +[NSObject(NSObject) instanceMethodSignatureForSelector:] + 0
3 CoreFoundation 0x0000000180495278 ___forwarding___ + 1280
4 CoreFoundation 0x000000018049759c _CF_forwarding_prep_0 + 92
5 Foundation 0x0000000180dde258 _NSCompareObject + 60
6 CoreData 0x00000001863c8564 +[NSFetchedResultsController _insertIndexForObject:inArray:lowIdx:highIdx:sortDescriptors:] + 220
7 CoreData 0x00000001863c82b4 -[NSFetchedResultsController _updateFetchedObjectsWithInsertChange:] + 832
8 CoreData 0x00000001863c9e00 __82-[NSFetchedResultsController(PrivateMethods) _core_managedObjectContextDidChange:]_block_invoke + 2364
9 CoreData 0x0000000186369e80 developerSubmittedBlockToNSManagedObjectContextPerform + 156
10 CoreData 0x0000000186369d5c -[NSManagedObjectContext performBlockAndWait:] + 212
11 CoreData 0x00000001863c94a8 -[NSFetchedResultsController _core_managedObjectContextDidChange:] + 96
12 CoreFoundation 0x00000001803c1878 __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 140
13 CoreFoundation 0x00000001803c179c ___CFXRegistrationPost_block_invoke + 84
14 CoreFoundation 0x00000001803c0c8c _CFXRegistrationPost + 404
15 CoreFoundation 0x00000001803c0668 _CFXNotificationPost + 688
16 Foundation 0x0000000180d84cb4 -[NSNotificationCenter postNotificationName:object:userInfo:] + 88
17 CoreData 0x000000018635ce54 -[NSManagedObjectContext _postObjectsDidChangeNotificationWithUserInfo:] + 320
18 CoreData 0x000000018636f640 -[NSManagedObjectContext _createAndPostChangeNotification:deletions:updates:refreshes:deferrals:wasMerge:] + 1244
19 CoreData 0x000000018635eb9c -[NSManagedObjectContext _processRecentChanges:] + 2884
20 CoreData 0x0000000186360cc0 -[NSManagedObjectContext save:] + 340
21 Playground 0x00000001024b8da8 $sSo22NSManagedObjectContextC10PlaygroundE12saveIfNeededyyKF + 108
22 Playground 0x00000001024b8eac $sSo22NSManagedObjectContextC10PlaygroundE9forceSaveyyF + 60
23 Playground 0x00000001024c3d30 $s10Playground8NameTileV4bodyQrvgyycfU_ + 1220
24 SwiftUI 0x00000001c5908800 OUTLINED_FUNCTION_11 + 620
25 SwiftUI 0x00000001c58365c0 OUTLINED_FUNCTION_31 + 1824
26 SwiftUI 0x00000001c51f742c OUTLINED_FUNCTION_21 + 32
27 SwiftUI 0x00000001c4f04840 OUTLINED_FUNCTION_2 + 6392
28 SwiftUI 0x00000001c4f0c7c4 OUTLINED_FUNCTION_2 + 39036
29 SwiftUI 0x00000001c51f742c OUTLINED_FUNCTION_21 + 32
30 SwiftUI 0x00000001c51f7448 OUTLINED_FUNCTION_21 + 60
31 SwiftUI 0x00000001c51f742c OUTLINED_FUNCTION_21 + 32
32 SwiftUI 0x00000001c58f6554 OUTLINED_FUNCTION_17 + 2340
33 SwiftUI 0x00000001c58f6b18 OUTLINED_FUNCTION_17 + 3816
34 SwiftUI 0x00000001c51e3f04 OUTLINED_FUNCTION_7 + 9760
35 SwiftUI 0x00000001c51e9298 OUTLINED_FUNCTION_7 + 31156
36 UIKitCore 0x0000000184a3583c -[UICollectionView _selectItemAtIndexPath:animated:scrollPosition:notifyDelegate:deselectPrevious:performCustomSelectionAction:] + 1176
37 UIKitCore 0x0000000184a650b4 -[UICollectionView touchesEnded:withEvent:] + 452
38 UIKitCore 0x000000018531a718 forwardTouchMethod + 264
39 UIKitCore 0x000000018531a718 forwardTouchMethod + 264
40 UIKitCore 0x000000018531a718 forwardTouchMethod + 264
41 UIKitCore 0x0000000184e30458 _UIGestureEnvironmentUpdate + 5912
42 UIKitCore 0x0000000184e2ea60 -[UIGestureEnvironment _deliverEvent:toGestureRecognizers:usingBlock:] + 288
43 UIKitCore 0x0000000184e2e7d0 -[UIGestureEnvironment _updateForEvent:window:] + 156
44 UIKitCore 0x0000000185329f00 -[UIWindow sendEvent:] + 3088
45 UIKitCore 0x000000018530998c -[UIApplication sendEvent:] + 576
46 UIKitCore 0x000000018538a5c0 __dispatchPreprocessedEventFromEventQueue + 1708
47 UIKitCore 0x000000018538d474 __processEventQueue + 5524
48 UIKitCore 0x0000000185385e38 __eventFetcherSourceCallback + 156
49 CoreFoundation 0x00000001803f1f18 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
50 CoreFoundation 0x00000001803f1e60 __CFRunLoopDoSource0 + 172
51 CoreFoundation 0x00000001803f15d0 __CFRunLoopDoSources0 + 232
52 CoreFoundation 0x00000001803ebcb8 __CFRunLoopRun + 768
53 CoreFoundation 0x00000001803eb5a4 CFRunLoopRunSpecific + 572
54 GraphicsServices 0x000000018e9fbae4 GSEventRunModal + 160
55 UIKitCore 0x00000001852f02e4 -[UIApplication _run] + 868
56 UIKitCore 0x00000001852f3f5c UIApplicationMain + 124
57 SwiftUI 0x00000001c51fc1b0 OUTLINED_FUNCTION_70 + 500
58 SwiftUI 0x00000001c51fc050 OUTLINED_FUNCTION_70 + 148
59 SwiftUI 0x00000001c4f02fa4 OUTLINED_FUNCTION_2 + 92
60 Playground 0x00000001024c9598 $s10Playground0A3AppV5$mainyyFZ + 40
61 Playground 0x00000001024c9648 main + 12
62 dyld 0x0000000102655544 start_sim + 20
63 ??? 0x00000001028660e0 0x0 + 4337328352
64 ??? 0xc719000000000000 0x0 + 14346498087965425664
)
Now here is my code, starting with data definition first:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22522" systemVersion="23C71" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
<entity name="ListEntity" representedClassName="ListEntity" syncable="YES">
<attribute name="name" optional="YES" attributeType="String"/>
<relationship name="items" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="ListItemEntity" inverseName="list" inverseEntity="ListItemEntity"/>
</entity>
<entity name="ListItemEntity" representedClassName="ListItemEntity" syncable="YES">
<attribute name="flag" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<relationship name="list" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ListEntity" inverseName="items" inverseEntity="ListEntity"/>
<relationship name="name" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NameEntity" inverseName="items" inverseEntity="NameEntity"/>
</entity>
<entity name="NameEntity" representedClassName="NameEntity" syncable="YES">
<attribute name="name" optional="YES" attributeType="String"/>
<relationship name="items" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="ListItemEntity" inverseName="name" inverseEntity="ListItemEntity"/>
</entity>
</model>
class DataProvider {
static let shared = DataProvider()
private let container: NSPersistentContainer
var viewContext: NSManagedObjectContext {
container.viewContext
}
private init() {
container = NSPersistentContainer(name: "Data")
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
container.loadPersistentStores { description, error in
print(description.url?.path(percentEncoded: false) ?? "No URL")
if let error {
fatalError("Could not load persistent stores: \(error.localizedDescription)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
}
}
extension NSManagedObjectContext {
func saveIfNeeded() throws {
guard hasChanges else { return }
try save()
}
func forceSave() {
do {
try saveIfNeeded()
} catch {
print(error.localizedDescription)
}
}
}
@objc(ListEntity)
public class ListEntity: NSManagedObject {}
public extension ListEntity {
@nonobjc class func fetchRequest() -> NSFetchRequest<ListEntity> {
NSFetchRequest<ListEntity>(entityName: "ListEntity")
}
@NSManaged var name: String?
@NSManaged var items: NSSet?
var wrappedName: String {
name ?? "No name"
}
var wrappedItems: [ListItemEntity] {
if let items = items as? Set<ListItemEntity> {
return items.sorted(using: KeyPathComparator(\.wrappedName))
}
return []
}
}
// MARK: Generated accessors for items
public extension ListEntity {
@objc(addItemsObject:)
@NSManaged func addToItems(_ value: ListItemEntity)
@objc(removeItemsObject:)
@NSManaged func removeFromItems(_ value: ListItemEntity)
@objc(addItems:)
@NSManaged func addToItems(_ values: NSSet)
@objc(removeItems:)
@NSManaged func removeFromItems(_ values: NSSet)
}
extension ListEntity: Identifiable {}
@objc(ListItemEntity)
public class ListItemEntity: NSManagedObject {}
public extension ListItemEntity {
@nonobjc class func fetchRequest() -> NSFetchRequest<ListItemEntity> {
NSFetchRequest<ListItemEntity>(entityName: "ListItemEntity")
}
@nonobjc class func fetchRequestFor(_ list: ListEntity, sortDescriptors: [NSSortDescriptor] = []) -> NSFetchRequest<ListItemEntity> {
let fetchRequest = Self.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "list == %@", list)
fetchRequest.sortDescriptors = sortDescriptors
return fetchRequest
}
@nonobjc class func fetchRequest(with name: NameEntity, on list: ListEntity, sortDescriptors: [NSSortDescriptor] = []) -> NSFetchRequest<ListItemEntity> {
let fetchRequest = Self.fetchRequest()
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
NSPredicate(format: "list == %@", list),
NSPredicate(format: "name == %@", name),
])
fetchRequest.sortDescriptors = sortDescriptors
return fetchRequest
}
@NSManaged var flag: Bool
@NSManaged var list: ListEntity?
@NSManaged var name: NameEntity?
var wrappedName: String {
name?.wrappedName ?? "No name"
}
}
extension ListItemEntity: Identifiable {}
@objc(NameEntity)
public class NameEntity: NSManagedObject {}
public extension NameEntity {
@nonobjc class func fetchRequest() -> NSFetchRequest<NameEntity> {
NSFetchRequest<NameEntity>(entityName: "NameEntity")
}
@nonobjc class func fetchRequest(containing name: String, fetchLimit: Int? = nil, sortDescriptors: [NSSortDescriptor] = []) -> NSFetchRequest<NameEntity> {
let fetchRequest = Self.fetchRequest()
if let fetchLimit {
fetchRequest.fetchLimit = fetchLimit
}
let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmedName.isEmpty {
fetchRequest.predicate = NSPredicate(format: "name CONTAINS[cd] %@", trimmedName)
}
fetchRequest.sortDescriptors = sortDescriptors
return fetchRequest
}
@NSManaged var name: String?
@NSManaged var items: NSSet?
var wrappedName: String {
name ?? "No name"
}
var wrappedItems: [ListItemEntity] {
if let items = items as? Set<ListItemEntity> {
return items.sorted(using: KeyPathComparator(\.wrappedName))
}
return []
}
}
// MARK: Generated accessors for items
public extension NameEntity {
@objc(addItemsObject:)
@NSManaged func addToItems(_ value: ListItemEntity)
@objc(removeItemsObject:)
@NSManaged func removeFromItems(_ value: ListItemEntity)
@objc(addItems:)
@NSManaged func addToItems(_ values: NSSet)
@objc(removeItems:)
@NSManaged func removeFromItems(_ values: NSSet)
}
extension NameEntity: Identifiable {}
Lastly my Views:
struct ListEntitiesView: View {
@Environment(\.managedObjectContext)
private var viewContext
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \ListEntity.name, ascending: true)],
animation: .default
)
private var lists: FetchedResults<ListEntity>
var body: some View {
NavigationStack {
List {
ForEach(lists) { list in
NavigationLink(value: list) {
ListEntityTile(list)
}
}
}
.navigationTitle("ListEntities")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
Button {
let list = ListEntity(context: viewContext)
list.name = "List \(lists.count + 1)"
viewContext.forceSave()
} label: {
Label("Add List", systemImage: "plus")
}
}
.navigationDestination(for: ListEntity.self) { list in
ListEntityView(list)
}
}
}
}
struct ListEntityTile: View {
@ObservedObject
private var list: ListEntity
@FetchRequest
private var items: FetchedResults<ListItemEntity>
private var totalItems: Int {
items.count
}
private var flaggedItems: Int {
items.filter(\.flag).count
}
init(_ list: ListEntity) {
_list = ObservedObject(wrappedValue: list)
_items = FetchRequest(fetchRequest: ListItemEntity.fetchRequestFor(list))
}
var body: some View {
HStack {
Text(list.wrappedName)
Spacer()
Text("\(flaggedItems) / \(totalItems)")
}
}
}
struct ListEntityView: View {
@Environment(\.managedObjectContext)
private var viewContext
@ObservedObject
private var list: ListEntity
@FetchRequest
private var items: FetchedResults<ListItemEntity>
init(_ list: ListEntity) {
_list = ObservedObject(wrappedValue: list)
_items = FetchRequest(fetchRequest: ListItemEntity.fetchRequestFor(list, sortDescriptors: [NSSortDescriptor(keyPath: \ListItemEntity.name, ascending: true)]))
}
var body: some View {
List {
ForEach(items) { item in
ListItemTile(item)
}
}
.navigationTitle(list.wrappedName)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
NavigationLink {
AddListItemEntity(list)
} label: {
Label("Add Item", systemImage: "plus")
}
}
}
}
struct ListItemTile: View {
@Environment(\.managedObjectContext)
private var viewContext
@ObservedObject
private var item: ListItemEntity
init(_ item: ListItemEntity) {
_item = ObservedObject(wrappedValue: item)
}
var body: some View {
Button {
item.flag.toggle()
viewContext.forceSave()
} label: {
Label(item.wrappedName, systemImage: item.flag ? "checkmark.circle" : "circle")
}
}
}
struct AddListItemEntity: View {
@Environment(\.managedObjectContext)
private var viewContext
@ObservedObject
private var list: ListEntity
@State
private var searchName = ""
@FetchRequest(sortDescriptors: [])
private var names: FetchedResults<NameEntity>
init(_ list: ListEntity) {
_list = ObservedObject(wrappedValue: list)
}
var body: some View {
List {
TextField("Search name", text: $searchName.animation())
NamesList(for: list, containing: searchName)
}
.toolbar {
Button {
let name = NameEntity(context: viewContext)
name.name = "Name \(names.count + 1)"
viewContext.forceSave()
} label: {
Label("Add Name", systemImage: "plus")
}
}
}
}
struct NamesList: View {
@ObservedObject
private var list: ListEntity
@FetchRequest
private var names: FetchedResults<NameEntity>
init(for list: ListEntity, containing name: String) {
_list = ObservedObject(wrappedValue: list)
_names = FetchRequest(
fetchRequest: NameEntity.fetchRequest(containing: name, sortDescriptors: [NSSortDescriptor(keyPath: \NameEntity.name, ascending: true)]),
animation: .default
)
}
var body: some View {
ForEach(names) { name in
NameTile(of: name, for: list)
}
}
}
struct NameTile: View {
@Environment(\.managedObjectContext)
private var viewContext
@FetchRequest
private var items: FetchedResults<ListItemEntity>
@ObservedObject
private var list: ListEntity
@ObservedObject
private var name: NameEntity
private var item: ListItemEntity? {
items.first { $0.name == name }
}
init(of name: NameEntity, for list: ListEntity) {
_list = ObservedObject(wrappedValue: list)
_name = ObservedObject(wrappedValue: name)
_items = FetchRequest(
fetchRequest: ListItemEntity.fetchRequestFor(list),
animation: .default
)
}
var body: some View {
Button {
if let item {
viewContext.delete(item)
} else {
let newItem = ListItemEntity(context: viewContext)
newItem.flag = false
newItem.name = name
newItem.list = list
}
viewContext.forceSave()
} label: {
Label(name.wrappedName, systemImage: item != nil ? "checkmark.circle" : "circle")
}
}
}
I thought that I must have somehow misconfigured ListItemEntity
to ListEntity
relationship, but I managed to run this code inside my DataProvider
and it worked without any problem:
let list = ListEntity(context: viewContext)
list.name = "List 1"
viewContext.forceSave()
var names = [NameEntity(context: viewContext), NameEntity(context: viewContext)]
for name in names {
name.name = "Name \(names.firstIndex(of: name) ?? 0)"
}
viewContext.forceSave()
let items = [ListItemEntity(context: viewContext), ListItemEntity(context: viewContext)]
for item in items {
item.flag = false
item.list = list
item.name = names.popLast()
}
viewContext.forceSave()
I believe it must be something simple and obvious as this is very simple use case I think, but my lack of experience get my to point I can't do anything more than ask for your help.
I found problem with my code. Problematic part was sort descriptor inside ListEntityView
. There was:
NSSortDescriptor(keyPath: \ListItemEntity.name, ascending: true)
Whereas it should be:
NSSortDescriptor(keyPath: \ListItemEntity.name?.name, ascending: true)
As the name
field in ListItemEntity
is a relationship thus we need use field from that related entity to be able to sort.