How to define CoreData relationship in Swift? - core-data

In CoreData, I have defined an unordered to-many relationship from Node to Tag. I've created an Swift entity like this:
import CoreData
class Node : NSManagedObject {
#NSManaged var tags : Array<Tag>
}
Now I want to add a Tag to an instance of Node, like this:
var node = NSEntityDescription.insertNewObjectForEntityForName("Node", inManagedObjectContext: managedObjectContext) as Node
node.tags.append(tag)
However, this fails with the following error:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Unacceptable type of value for to-many relationship: property = "tags"; desired type = NSSet; given type = _TtCSs22ContiguousArrayStorage000000000B3440D4; value = (
"<_TtC8MotorNav3Tag: 0xb3437b0> (entity: Tag; id: 0xb343800 ; data: {...})"
).'
What is the correct type for to-many relationships?

To be able to work with one-to-many relationship in Swift you need to define property as:
class Node: NSManagedObject {
#NSManaged var tags: NSSet
}
If you try to use NSMutableSet changes will not be saved in CoreData. And of course it is recommended to define reverse link in Node:
class Tag: NSManagedObject {
#NSManaged var node: Node
}
But still Swift cannot generate dynamic accessors in runtime, so we need to define them manually. It is very convenient to define them in class extension and put in Entity+CoreData.swift file. Bellow is content of Node+CoreData.swift file:
extension Node {
func addTagObject(value:Tag) {
var items = self.mutableSetValueForKey("tags");
items.addObject(value)
}
func removeTagObject(value:Tag) {
var items = self.mutableSetValueForKey("tags");
items.removeObject(value)
}
}
Usage:
// somewhere before created/fetched node and tag entities
node.addTagObject(tag)
Important: To make it all work you should verify that class names of entities in you CoreData model includes your module name. E.g. MyProjectName.Node

As of Xcode 7 and Swift 2.0, the release note 17583057 states:
The NSManaged attribute can be used with methods as well as
properties, for access to Core Data’s automatically generated
Key-Value-Coding-compliant to-many accessors.
#NSManaged var employees: NSSet
#NSManaged func addEmployeesObject(employee: Employee)
#NSManaged func removeEmployeesObject(employee: Employee)
#NSManaged func addEmployees(employees: NSSet)
#NSManaged func removeEmployees(employees: NSSet)
These can be declared in your NSManagedObject subclass. (17583057)
So you just have to declare the following methods and CoreData will take care of the rest:
#NSManaged func addTagsObject(tag: Tag)
#NSManaged func removeTagsObject(tag: Tag)
#NSManaged func addTags(tags: NSSet)
#NSManaged func removeTags(tags: NSSet)

Actually you can just define:
#NSManaged var employees: Set<Employee>
And use the insert and remove methods of the Set directly.

Building on #Keenle's answer, if you want to be cheeky and concise and be able to say
node.tags.append(tag)
one can wrap the call to self.mutableSetValueForKey:
class Node: NSManagedObject {
var tags: NSMutableOrderedSet {
return self.mutableOrderedSetValueForKey("tags")
}
}

Related

Crash when delete a record from coredata in SwiftUI

I make a list for audio items from coredata. after deleting, crash reported as "EXC_BREAKPOINT (code=1, subcode=0x1b8fb693c)", why?
When using
ForEach(items, id: \.self)
, it works. But My Audio has id property and follow Identifiable protocol.
UPDATE: I found adding a if{} clause will fix crash, but why? Breakpoint at "static UUID.unconditionallyBridgeFromObjectiveC(:) ()".
struct Test1View: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(fetchRequest: Audio.fetchAllAudios()) var items: FetchedResults<Audio>
var body: some View {
List {
ForEach(items) { item in
if true { // <- this if clause fix crash, but why?
HStack {
Text("\(item.name)")
}
}
}.onDelete(perform: { indexSet in
let index = indexSet.first!
let item = self.items[index]
self.context.delete(item)
try? self.context.save()
})
}
}
}
code as following:
class Audio: NSManagedObject, Identifiable {
#NSManaged public var id: UUID
#NSManaged public var name: String
#NSManaged public var createAt: Date
}
struct Test1View: View {
#Environment(\.managedObjectContext) var context
var fetchRequest: FetchRequest<Audio> = FetchRequest<Audio>(entity: Audio.entity(), sortDescriptors: [NSSortDescriptor(key: "createAt", ascending: false)])
var items: FetchedResults<Audio> { fetchRequest.wrappedValue }
var body: some View {
List {
ForEach(items) { item in
HStack {
Text("\(item.name)")
}
}.onDelete(perform: { indexSet in
let index = indexSet.first!
let item = self.items[index]
self.context.delete(item)
try? self.context.save()
})
}
}
}
I had the same problem as you.
Probably it is a bad idea to use your own id property to make it Identifiable because Core Data is setting all the properties to nil when the object is deleted, even if you declared it differently in your Swift CoreData class.
When deleting your entity, the id property gets invalidated and the objects .isFault property is set to true, but the SwiftUI ForEach still holds some reference to this ID object (=your UUID) to be able to calculate the "before" and "after" state of the list and somehow tries to access it, leading to the crash.
Therefore the following recommendations:
Protect the detail view (in the ForEach loop by checking isFault:
if entity.isFault {
EmptyView()
}
else {
// your regular view body
}
Expect your id property to be nil, either by defining it accordingly in your core data model as optional
#NSManaged public var id: UUID?
or by not relying on the Identifiable protocol in the SwiftUI ForEach loop:
ForEach(entities, id: \.self) { entity in ... }
or
ForEach(entities, id: \.objectID) { entity in ... }
Conclusion: you really do not need to make all your CoreData properties Swift Optionals. It's simply important that your id property referenced in the ForEach loop handles the deletion (=setting its value to nil) gracefully.
I found the reason of crash, must provide optional, because of OC/swift object conversion:
convert
class Audio: NSManagedObject, Identifiable {
#NSManaged public var id: UUID
#NSManaged public var name: String
#NSManaged public var createAt: Date
}
to
class Audio: NSManagedObject, Identifiable {
#NSManaged public var id: UUID?
#NSManaged public var name: String?
#NSManaged public var createAt: Date?
}
I had the same issue over the weekend. It looks like SwiftUI want's to unwrap the value i read from CoreData and as the value is already deleted it crashes.
In my case i did solve it with nil coalescing on all values i use from CoreData.
You can try to provide a default value on your item.name with
ForEach(items) { item in
HStack {
Text("\(item.name ?? "")")
}
}

Setting Bool attribute of an NSManagedObject throws "Uncaught exception in iOS 10"

I am upgrading my app to iOS 10 / swift 3
So constructing an NSManagedObject with a dictionary (values received from remote db) this way...
let writerDictionary = [
...: writer.id,
...: writer.name,
...: writer.picture,
...: writer.publicationID,
...: writer.language,
...: writer.country
]
let newWriter = Writer(dictionary: writerDictionary, context: SHARED_CONTEXT)
isFavorite is an NSManaged property that I set manually to true with newWriter.isFavorite = true right after constructing the newWriter object before saving the context -
This line crashes with Uncaught Exception in iOS 10 - Previously there was no problem whatsoever with iOS 9
I have also tried newWriter.setValue(true, forKey: "isFavorite") - While it does not crash the app, the bool value isFavorite remains false unchanged.
Any ideas? This is my Writer subclass of NSManagedObject
import UIKit
import CoreData
#objc(Writer)
class Writer: NSManagedObject {
// Attributes also in the database
#NSManaged var id: NSNumber
#NSManaged var name: String
#NSManaged var picture: String?
#NSManaged var publicationID: NSNumber
#NSManaged var language: String
#NSManaged var country: String
#NSManaged var lastArticle: Date
// Attributes only in the iOS app
#NSManaged var isFavorite: Bool // Initial value is false
#NSManaged var hasNewArticles: Bool
// Relationship objects
#NSManaged var publication: Publication
#NSManaged var articles: [Article]
var writerImage: UIImage? {
get {
return ImageCache.sharedCache.imageWithIdentifier("writers-" + String(id))
}
set {
ImageCache.sharedCache.storeImage(newValue, withIdentifier: "writers-" + String(id))
}
}
override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
init(dictionary: [String : AnyObject], context: NSManagedObjectContext) {
let entity = NSEntityDescription.entity(forEntityName: "Writer", in: context)!
super.init(entity: entity, insertInto: context)
id = ...
name = ...
picture = ...
publicationID = ...
language = ...
country = ...
lastArticle = ...
}
override func prepareForDeletion() {
if let _ = writerImage {
writerImage = nil
}
}
}
Solution
The current version of Swift 3 beta seems to have some flaw about treating properties prefixed with "is".
Adjusting the name of the property like this, solved it...
#NSManaged #objc(isFavorite) var isFavorite: Bool
Fixed in Xcode 8 beta 3 compiler.
From the release notes:
Earlier Swift 3 betas implicitly renamed the property, getter, and
setter for #objc properties of type Bool whose names started with
“is”, to match the Objective-C naming conventions. (For example, a
Swift boolean property named isMagical generated an Objective-C
declaration of the form #property (nonatomic, getter=isMagical) BOOL
magical.) This turned out to cause more trouble than it was worth and
has been removed. (26847223)

Resolving 'Failed to call designated initializer on NSManagedObject class'

I'm new to Swift and I'm trying to learn how to use Core Data. But I'm getting this error and I'm not sure what I've done wrong. I've searched online and tried a few things but I can't get it right.
Failed to call designated initializer on NSManagedObject class 'FirstCoreData.Course'
When this line executes:
ncvc.currentCourse = newCourse
In this function:
class TableViewController: UITableViewController, AddCourseViewControllerDelegate {
var managedObjectContext = NSManagedObjectContext.init(concurrencyType: NSManagedObjectContextConcurrencyType.MainQueueConcurrencyType)
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "addCourse" {
let ncvc = segue.destinationViewController as! NewCourseViewController
ncvc.delegate = self
let newCourse = NSEntityDescription.insertNewObjectForEntityForName("Course", inManagedObjectContext: self.managedObjectContext) as! Course
ncvc.currentCourse = newCourse
}
}
Class generated by "Create NSManagedObject Subclass..." for Course entity:
import Foundation
import CoreData
class Course: NSManagedObject {
// Insert code here to add functionality to your managed object subclass
}
And:
import Foundation
import CoreData
extension Course {
#NSManaged var title: String?
#NSManaged var author: String?
#NSManaged var releaseDate: NSDate?
}
The problem lies not in the code in your question, but in the snippet you included as comments to the other answer:
var currentCourse = Course()
This doesn't just declare currentCourse to be of type Course, it also creates an instance of the Course entity using the standard init method. This is expressly not allowed: You must use the designated initialiser: init(entity entity: NSEntityDescription,
insertIntoManagedObjectContext context: NSManagedObjectContext?). This is described in the Apple Documentation here.
I suspect you do not ever use the instance created by the above var definition, so just define it as being of type Course?:
var currentCourse : Course?
Since it is optional, you do not need to set an initial value, though you will need to unwrap the value whenever it is used.
The simplest way is this:
Define in the applicationDelegate a reference for the context
Instantiate the variable by passing the context
In the AppDelegate (outside the brackets):
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
And in the code:
let currentCourse = Course(context:context)
Now you have your entity created. But don't forget to save with:
appDelegate.saveContext()
I had the same issue. And instantiating the object like this worked, for your course it would be something like this:
var currentCourse = Course.init(entity: NSEntityDescription.entityForName("Course", inManagedObjectContext:mox)!, insertIntoManagedObjectContext: mox)
instead of:
var currentCourse = Course()
I used this in Xcode 8.3.2 with Swift 3.1.
NSEntityDescription.insertNewObject(forEntityName: String(describing: type(of: Record())), into: managedObjectContext) as! Record
And got the same error message. But this data was inserted into db. So maybe this doesn't matter.
Your currentCourse should be NSManagedObject class
Please refer this CoreData: error: Failed to call designated initializer on NSManagedObject class

NSManagedObject subclasses in Swift can not use custom accessor?

I am using Swift in Core Data generated subclass of NSManagedObject. There is a transient optional property title.(The optional is not Swift's optional, but Core Data's optional.)
So I need a custom getter. My code is
class ShoppingList: NSManagedObject {
#NSManaged var title: String
func title() -> String {
return "something"
}
}
The Objective-C version of the getter works fine. However, Xcode tells me that "the func title() is an invalid redeclaration". I tried to use computed property, but get that "#Managed property can not use computed property".
So my question is, is there an alternative way to get custom accessors(getters) in Swift version of NSManagedObject subclassing?
You could use a different name for a computed property, and have it return the title variable.
#NSManaged var title: String
var myTitle : String {
return self.title
}
Would that work for you?
Apple does it like this in many places by naming the actual var with an underscore in front, and the computed property with the same name but without the underscore

Swift / Core Data / Bool or ObjCBool

I'm trying to save my model with fields of type bool. When I try to add the "true" value an error occurs.
I also tried to change the field type to Objective C Bool (ObjCBool) unsuccessfully, someone had this problem too?
import UIKit
import CoreData
#objc(Test)
class Test: NSManagedObject {
#NSManaged var title: String
#NSManaged var field: ObjCBool // Or Bool
}
// the error occurs before saving at this point
test.field = true
solution with numberWithBool:
import UIKit
import CoreData
#objc(Test)
class Test: NSManagedObject {
#NSManaged var title: String
#NSManaged var field: NSNumber
}
test.field = NSNumber.numberWithBool(false)

Resources