Swift set or update asset in core data - core-data

Two Entities
Gymnast is one to many to Meet
I would like to when I save a new meet, it gets assigned to as a meet to each gymnast where they can then score their individuals scores for each event
Maybe I completely wrong in my logic, but here is what I am trying to do
let request = NSFetchRequest(entityName: "Gymnast")
do {
let entities = try AD.managedObjectContext.executeFetchRequest(request) as! [Gymnast]
for item in entities {
if let first = item.valueForKey("firstName"), last = item.valueForKey("lastName") {
print("Name: \(first) \(last)")
let myMeet = NSEntityDescription.insertNewObjectForEntityForName("Meet", inManagedObjectContext: AD.managedObjectContext) as! Meet
myMeet.meetName = "Winter Classic"
let myMeets = item.meets!.mutableCopy() as! NSMutableSet
myMeets.addObject(myMeet)
item.meets = myMeets.copy() as? NSSet
AD.saveContext()
}
}
} catch {
}
}
}

I think it is not ideal to replicate the Meet object over and over again for each Gymnast. For example, the meetName will be stored multiple times. I am not sure this is intended.
However, going with your setup, your problem is how you assign the to-many relationship. For a one-to-many it is always easier to simply set the to-one relationship. (Remember, there is always a reverse relationship in the Core Data model.)
Thus,
myMeet.gymnast = item
is all you need.
To add and remove to-many relationships, you can use this extension:
// Support adding to many-to-many relationships
extension NSManagedObject {
func addObject(value: NSManagedObject, forKey key: String) {
let items = self.mutableSetValueForKey(key)
items.addObject(value)
}
func removeObject(value: NSManagedObject, forKey key: String) {
let items = self.mutableSetValueForKey(key)
items.removeObject(value)
}
}

Related

Swift 4 Core Data - Fetching Relationships

I have entities as so:
Entity 1
Entity 2
I am saving data as so:
#IBAction func saveButton(_ sender: UIButton) {
print("save")
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
let context = appDelegate.persistentContainer.viewContext
let workout = Workout(context: context)
workout.name = workoutName.text
workout.noOfSets = Int16(setsStepper.value)
for index in 0..<setsVal {
let sets = Sets(context: context)
let test = IndexPath(row: index, section: 0)
let cell = tableView.cellForRow(at: test) as! RepsTableViewCell
sets.repAmount = Int16(cell.repsStepper.value)
// Does this line not create the relationship between Workout and Set Entities?
workout.addToSets(sets)
}
try! context.save()
}
And I am fetching data as so:
func fetch() {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
let context = appDelegate.persistentContainer.viewContext
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Workout")
request.relationshipKeyPathsForPrefetching = ["Sets"]
do {
let result = try context.fetch(request)
for data in result as! [NSManagedObject] {
print(data.value(forKey: "name") as! String)
print(data.value(forKey: "noOfSets") as! Int16)
}
}
catch {
print("failed")
}
}
I've set up the relationship between entities Workout and Sets as one-many, yet cannot retrieve Set's attributes from Workout.
How can I retrieve an entities relationship attributes?
Do I need to specify the relationship programmatically despite the relationship being setup in the xcdatamodel file?
Does Workout.addToSets(sets) create the relationship between the entities?
You should be able to access your relationship entities as an attribute so for instance
for workout in result as! [Workout] {
print(workout.name)
if let sets = workout.sets { //assuming you have name your to-many relationship 'sets'
print(sets.count)
}
}

doubled savings with core data swift 4

developing a little app for my comic collection encountered this issue:
in my second "add comic" VC I have a button and the func below, but I save TWICE entities in manged context (ate least, I think this is the issue)
for example if I have 2 comics yet shown in main VC tableview, go to "add comic VC" and save a third one, going back to main VC I'll print 3 objects with title, number etc but also print 2 new objects with no data as I had saved twice a manger context a "right one" and another one with same number of object but empty. If I keep adding a 4th comic, I'll get 6 complete comic + the 4th and more 6 "blank itmes" with default values "no title"
let kComicEntityName = "Comic"
func addingSingleComic(gotTitle: String, gotIssue: Int16, gotInCollection: Bool ) {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {return}
let managedContext = appDelegate.persistentContainer.viewContext
let entity = NSEntityDescription.entity(forEntityName: kComicEntityName, in: managedContext)!
let comicToAdd = Comic(entity: entity, insertInto: managedContext)
comicToAdd.comicTitle = gotTitle
comicToAdd.issueNumber = gotIssue
comicToAdd.inCollection = gotInCollection
do {
try managedContext.save()
} catch let error as NSError {
print("could not save. \(error), \(error.userInfo)")
}
print("new single comic crated: title: \(comicToAdd.comicTitle ?? "!! not title !!"), n. \(comicToAdd.issueNumber), owned?: \(comicToAdd.inCollection)")
}
in the main VC I use this to check items in core data
func asyncPrintEntities() {
self.asyncComicEntityArray.removeAll(keepingCapacity: true)
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {return}
let managedContext = appDelegate.persistentContainer.viewContext
let comicFetch : NSFetchRequest<Comic> = Comic.fetchRequest()
asyncFetchRequest = NSAsynchronousFetchRequest<Comic>(fetchRequest: comicFetch) {
[unowned self] (result: NSAsynchronousFetchResult) in
guard let AllComicEntityResult = result.finalResult else {
return
}
self.asyncComicEntityArray = AllComicEntityResult
//************************************
do {
self.asyncComicEntityArray = try managedContext.fetch(comicFetch)
if self.asyncComicEntityArray.count > 0 {
print("Ok! model is not empty!")
} else {
print("No entites availabe")
}
} catch let error as NSError {
print("Fetch error: \(error) description: \(error.userInfo)")
}
guard self.asyncComicEntityArray != nil else {return}
for comicFoundInArray in self.asyncComicEntityArray {
let entity = NSEntityDescription.entity(forEntityName: self.kComicEntityName, in: managedContext)!
var comicTouse = Comic(entity: entity, insertInto: managedContext)
// var comicTouse = Comic() //to be deleted since this kind of init is not allowed, better above insertInto
comicTouse = comicFoundInArray as! Comic
print("comic title: \(comicTouse.comicTitle ?? "error title"), is it in collection? : \(comicTouse.inCollection)")
}
self.MyTableView.reloadData()
//************************************
}
// MARK: - async fetch request 3
do {
try managedContext.execute(asyncFetchRequest)
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
//end of function
}
In your addingSingleComic you create a new Comic here:
let comicToAdd = Comic(entity: entity, insertInto: managedContext)
Then you assign values to the object's properties.
Separately, in asyncPrintEntities, you also create new Comic objects here:
var comicTouse = Comic(entity: entity, insertInto: managedContext)
This time you do not assign values to the object's properties. They will have no title, etc, because you created them but never assigned a title. This line executes once for every object in asyncComicEntityArray, so if the array has two objects, you create two new objects that contain no data. You don't use comicToUse anywhere except in the one print, but it still exists in the managed object context and will still get saved the next time you save changes.
This is why you're getting extra entries-- because you're creating them in this line of code. It's not clear why you're creating them here. You just executed a fetch request, and then you immediately create a bunch of no-data entries which you don't use. It looks like that entire for loop could just be deleted, because the only thing it does is create these extra entries.

Deleting all data in a Core Data entity in Swift 3

Is there a way to do a batch delete of all data stored in all of the entities in core data?
I read somewhere that in iOS 9 or 10 that apple introduced a way to do batch deletes, but I can't seem to find any good information on it.
Ultimately, I just need a function that goes through an entity, and deletes all of the data in it. Seems like it should be simple enough, but documentation/tutorials for it have proven exceedingly difficult to find.
Any thoughts?
Edit
I added the following code into an IBAction attached to a button:
#IBAction func clearAllData(_ sender: AnyObject) {
let fetch = NSFetchRequest<NSFetchRequestResult>(entityName: "PLProjects")
let request = NSBatchDeleteRequest(fetchRequest: fetch)
//get the data from core data
getPLData()
//reload the table view
tableView.reloadData()
}
This does not seem to work however. If I close down the project and reopen it, the data is still there. I am assuming this is also why the table view doesn't update, because the data is not actually being deleted.
You're thinking of NSBatchDeleteRequest, which was added in iOS 9. Create one like this:
let fetch = NSFetchRequest<NSFetchRequestResult>(entityName: "Employee")
let request = NSBatchDeleteRequest(fetchRequest: fetch)
You can also add a predicate if you only wanted to delete instances that match the predicate. To run the request:
let result = try managedObjectContext.executeRequest(request)
Note that batch requests don't update any of your current app state. If you have managed objects in memory that would be affected by the delete, you need to stop using them immediately.
To flesh out Tom's reply, this is what I added to have a complete routine:
func deleteAllRecords() {
let delegate = UIApplication.shared.delegate as! AppDelegate
let context = delegate.persistentContainer.viewContext
let deleteFetch = NSFetchRequest<NSFetchRequestResult>(entityName: "CurrentCourse")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: deleteFetch)
do {
try context.execute(deleteRequest)
try context.save()
} catch {
print ("There was an error")
}
}
Declare the Method for getting the Context in your CoreDataManager
Class
class func getContext() -> NSManagedObjectContext {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
}
if #available(iOS 10.0, *) {
return appDelegate.persistentContainer.viewContext
} else {
return appDelegate.managedObjectContext
}
}
Call the above method from your NSManagedObject subClass:
class func deleteAllRecords() {
//getting context from your Core Data Manager Class
let managedContext = CoreDataManager.getContext()
let deleteFetch = NSFetchRequest<NSFetchRequestResult>(entityName: "Your entity name")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: deleteFetch)
do {
try managedContext.execute(deleteRequest)
try managedContext.save()
} catch {
print ("There is an error in deleting records")
}
}

How do I loop through an NSSet of Core Data

I have a User entity and a Hobby entity and a relationhip of to-many where one User can have many Hobbies. I am adding the user to a hobby like this, `#IBAction func addHobby(sender: AnyObject) {
let appDel:AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let context:NSManagedObjectContext = appDel.managedObjectContext
var newHobby = NSEntityDescription.insertNewObjectForEntityForName("Hobby", inManagedObjectContext: context) as! Hobby
newHobby.name = addHobbyTextField.text
newHobby.user = curUser
do{
try context.save()
} catch{
print("There was a problem \(error)")
}
}`
but I am trying to figure out how to loop through the User entity object to see what hobbies a certain user has. I am using this currently but its not working,
#IBAction func printUser(sender: AnyObject) {
print(curUser!.hobby! as NSSet)
}
I get this in the console:
Relationship 'hobby' fault on managed object (0x7fef13674e10) <Core_Data_Demo.Users: 0x7fef13674e10> (entity: Users; id: 0xd000000000040000 <x-coredata://5E62A797-4124-4816-8AC0-32C5E723F830/Users/p1> ; data: {
hobby = "<relationship fault: 0x7fef134515c0 'hobby'>";
password = test;
username = free;
})
Any help would be awesome, I'm stuck.
The relationship will be a fault until you access it. That is what you are seeing in your print. You need to walk the relationship to see what is in it:
for item in curUser.hobby {
print(item)
}
That will cause the relationship to be accessed which will realize the relationship and give you a set of objects to walk.
Note that even with that each hobby will be a fault so you may want to touch a property on the hobby instead of printing the entire object:
for item in curUser.hobby {
print(item.name)
}
As an example.
I would suggest reading up on faulting in Core Data

NSFetchRequest with NSPredicate returning correct results, but whose properties aren't updated

My managedObjectContext hierarchy is as follows: (PSC)<-(writerMOC -- private)<-(mainMOC -- main)<-(backgroundMOC -- private)
I have an NSManagedObject who "name" property is "Banana".
In the backgroundMOC, I get a reference to the object with backgroundMOC.objectWithID, change the NSManagedObject's "name" property to "Apple", and subsequently set it's "syncStatus" property to 1 (flagged for synchronization), then recursively save the moc's with the following routine:
func saveManagedContext(moc: NSManagedObjectContext, shouldSync: Bool = true, completion: (() -> Void)? = nil)
{
print("\nSaving managed object context...")
do {
try moc.save()
if let parentContext = moc.parentContext {
parentContext.performBlock {
self.saveManagedContext(parentContext, shouldSync: shouldSync, completion: completion)
}
}
else {
if shouldSync { SyncEngine.sharedInstance.synchronize(shouldPushUpdates: true) }
completion?()
}
print("Finished saving managed object context...")
} catch {
logger.error("\(error)")
}
}
Once the last moc is saved, a sync routine is called which does its work on the backgroundMOC, which queries the local store for all objects whose syncStatus is 1, again this fetch is called on the backgroundMOC.
let fetchRequest = NSFetchRequest(entityName: entity.name)
let syncPredicate = NSPredicate(format: "%K == %d", JSONKey.SyncStatus.rawValue, 1)
fetchRequest.predicate = syncPredicate
return try backgroundMOC.executeFetchRequest(fetchRequest) as? [SyncableManagedObject] ?? []
This correctly returns the updated object in the array, however, that object's syncStatus property equals 0, and its "name" property is still set to "Banana".
This is really causing me headaches, I felt like i had totally understood how managedObjectContext blocks should work, but this has proven to be quite a puzzle.
UPDATE
Here's the code that prompts the update. This is called from the main thread when the cell is tapped.
func updateNameForCell(cell: UITableViewCell)
{
///gets the object id from the fetchedResultsController
guard let fruitMetaID = tableController.objectIDForCell(cell) else { return }
let backgroundMOC = CoreDataController.sharedInstance.newBackgroundManagedObjectContext()
backgroundMOC.performBlock {
do {
guard let fruit = (backgroundMOC.objectWithID(fruitMetaID) as? FruitMetaData)?.fruit else {
throw //Error
}
print(fruit.name) // "Banana"
fruit.name = "Apple"
fruit.needsSynchronization() //Sets syncStatus to 1
CoreDataController.sharedInstance.saveManagedContext(backgroundMOC)
}
catch {
//handle error
}
}
}
UPDATE AGAIN
Maybe I'm not creating the contexts right. Enlighten me please!
/// The parent to all other NSManagedObjectContexts. Responsible for writting to the store.
lazy var writerManagedObjectContext: NSManagedObjectContext =
{
let managedObjectContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
managedObjectContext.performBlockAndWait {
managedObjectContext.persistentStoreCoordinator = self.persistentStoreCoordinator
}
return managedObjectContext
}()
lazy var mainManagedObjectContext: NSManagedObjectContext =
{
let managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
managedObjectContext.performBlockAndWait {
managedObjectContext.parentContext = self.writerManagedObjectContext
}
return managedObjectContext
}()
/// The context associated with background syncing..
func newBackgroundManagedObjectContext() -> NSManagedObjectContext
{
let backgroundManagedObjectContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
backgroundManagedObjectContext.performBlockAndWait {
backgroundManagedObjectContext.parentContext = self.mainManagedObjectContext
}
return backgroundManagedObjectContext
}
Holding onto child MOCs (children of the main context) is fraught with issues. I would recommend creating a new child (aka backgroundMOC) for each operation that you do.
Without seeing all of your code this looks like an issue with the child context getting out of sync.
Update
Assuming that your creation of the backgroundMOC sets the mainMOC as its parent then I wonder about the -objectWithID: and what it is returning.
I also wonder about your -performBlock: calls. In my head the threading looks fine but better to test. Try changing to -performBlockAndWait: just to test and see if there is a threading race condition. Not a permanent change but eliminates that part of the code as a source of the issue.
Before fetchRequest is called, you should reset context.
backgroundMOC.reset() // add this line
let fetchRequest = NSFetchRequest(entityName: entity.name)
let syncPredicate = NSPredicate(format: "%K == %d", JSONKey.SyncStatus.rawValue, 1)
fetchRequest.predicate = syncPredicate
return try backgroundMOC.executeFetchRequest(fetchRequest) as? [SyncableManagedObject] ?? []
The reason is FruitMetaData is an object(or class) so changing one of its properties/Core Data attributes does not register as a change to the results array ... the object references in the array remain the same.
And NSFetchRequest still returns the same result(by using cache). When use context.reset().This tells the context in the extension to fetch new data every time and ignore the cache.

Resources