NSManagedObject subclass' category equivalent in Swift? - ios

I just started Swift and usually in Objective-C I create a category for each one of my NSManagedObject subclasses so the category isn't erased each time I have to generate my subclass. Moreover, it can simplify the creation of CoreData objects, especially when populated by data coming from a JSON.
Example :
My NSManagedObject subclass :
#class Book;
#interface Book : NSManagedObject
#property (nonatomic, retain) NSString * name;
#property (nonatomic, retain) NSString * author;
#property (nonatomic, retain) NSString * plot;
#end
Its "helper" category implementation :
+ (Book*)bookFromDictionary:(NSDictionary *)dictionary inContext:(NSManagedObjectContext *)context {
Book *book = nil;
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:#"Book"];
request.predicate = [NSPredicate predicateWithFormat:#"name == %#", dictionary[#"name"]];
request.fetchLimit = 1;
NSError *error;
NSArray *result = [context executeFetchRequest:request error:&error];
if (!result || error) {
NSLog(#"Error getting Book : %#", error.localizedDescription);
} else if ([result count] == 0) {
book = [NSEntityDescription insertNewObjectForEntityForName:#"Book" inManagedObjectContext:context];
} else {
book = [result lastObject];
}
self.name = dictionary[#"name"];
self.author = dictionary[#"author"];
self.plot = dictionary[#"plot"];
return book;
}
I'm trying to reproduce this concept in Swift but I don't have any idea how.
It seems that extensions replace categories in Swift but if I implement an extension into a NSManagedObject subclass in Swift, it will be erased each time I have to generate my NSManagedObject subclass (because it's in the same file...). And that's why I used to create categories in Obj-C.
Can someone tell me what are the good practices about this in Swift ?
I would greatly appreciate any help !

A Swift extension is the right way to define additional methods for NSManagedObject subclasses.
You can put the extension into a separate file
(e.g. "BookExtensions.swift") so that they are not overwritten when
"Book.swift" is recreated in Xcode.

There are issues in generating the subclasses for NSManagedObjects in Swift. Typically, you have to go through them again manually to e.g. change types into optionals, include markers like #objc(Class) etc.
Therefore, I have changed my workflow: I am now including the custom methods in the generated file without using an extension (which is the Swift equivalent of a category). When updating my managed object model, I just change the the file marginally by adding, renaming, changing etc. the attributes and relationships.

Related

How to Store JSON Data in Core Data in iOS

I am getting the response from JSON is stored in an array and I want to store the values
{albumId albumName coverPhotoURL createdDate} in Core Data please help me.
(
{
albumId = 1;
albumName = UAE;
coverPhotoURL = "http://1-dot-digiphoto-01.appspot.com/serve?blob-key=AMIfv95XeG-ii4aKZsUB5w-ClP0QUhJZa-o5BQRvdqArCCwg0Ueb13-wAfmyNHgaDdTaFS152_kXkJg5_9386zlfRCDc3fagW7Ekagdd6_VvJl6IscqNkyvVkXKYAqIRe-KqDMpjG-cW";
createdDate = "10-Jun-2010 06:11 PM";
description = "photos took in Dubai";
lastViewedDate = "10-Jun-2010 06:11 PM";
modifiedDate = "10-Jun-2010 06:11 PM";
numberOfPhotos = 10;
}
)
You need to create a subclass of NSManagedObject and define all needed fields
#interface AlbumInfo : NSManagedObject
#property (nonatomic, retain) NSNumber * albumId;
#property (nonatomic, retain) NSString * albumName;
#property (nonatomic, retain) NSString * coverPhotoURL;
#property (nonatomic, retain) NSDate * createdDate;
#end
then you need to call this code
context = /* Get the NSManagedObject context */
AlbumInfo *item = [NSEntityDescription insertNewObjectForEntityForName:#"AlbumInfo" inManagedObjectContext:context];
item.albumId = #"some value from json";
/* ... so on ...*/
NSError *error = nil;
if (context != nil) {
if ([managedObjectContext hasChanges] && ![context save:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
}
I Suppose you created an Entity That fits the data you need to store.
Then Create NSEntityDescription and NSManagedObject to start load your object into Core Data.
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:#"ALbum" inManagedObjectContext:self.managedObjectContext];
NSManagedObject *newAlbum = [[NSManagedObject alloc] initWithEntity:entityDescription insertIntoManagedObjectContext:self.managedObjectContext];
Then you should set values into managed object
[newPerson setValue:#"photos took in Dubai" forKey:#"description"];
[newPerson setValue:#" UAE" forKey:#"albumName"];
Last Step you should save These changes like this
NSError *error = nil;
if (![newAlbum.managedObjectContext save:&error]) {
NSLog(#"Unable to save managed object context.");
NSLog(#"%#, %#", error, error.localizedDescription);
}
I would like to suggest importing data using MagicalRecord an excellent wrapper for CoreData. It provides convention over configuration method of data import.
The importing feature is capable of mapping the data even if the properties are different in your json model compared to entity or if you would like to convert date represented in string to NSDate
To avoid duplicate entries, MagicalRecord makes it a convention to use a uniqueID. i.e., It expects a uniqueID property for an entity Album like albumID, Employee - employeeID etc. So one need to model it as per convention. As per your case model would be like this.
You can note that in the User Info of albumID, the property is mapped to albumId. Similarly the createdDate is not holding a string value but a NSDate.
A dateFormat is set to the UserInfo of createdDate
Now the configuration part is completed. You can import the date to CoreData as in
NSData *jsonData = //Data from web service;
NSArray *albums = [NSJSONSerialization JSONObjectWithData:jsonData
options:0
error:nil];
for (id obj in albums) {
[Album MR_importFromObject:obj];
}
Check if data is properly imported using
NSArray *savedAlbums = [Album MR_findAll];
Album *album = [savedAlbums lastObject];
NSLog(#"Created Date : %#",album.createdDate);

Fetch objects in Core Data through one to many relationship

I have this One-to-Many relationship in Core Data:
Each SBQChrono can have many SBQLaps.
In my model I have the class SBQLap:
#interface CILap : NSObject
#property (strong, nonatomic) NSDate * lapDate;
#end
And the class SBQChrono:
#interface CIChrono : NSObject
#property (strong, nonatomic) NSDate * startDate;
#property (strong, nonatomic) NSDate * stopDate;
#property (strong, nonatomic) NSOrderedSet *laps;
#end
I get all the SBQChrono entities doing:
NSFetchRequest *request=[[NSFetchRequest alloc] initWithEntityName:kChronoEntityName];
NSError *error;
NSArray *objects = [appContext.managedObjectContext executeFetchRequest:request error:&error];
How can I get the specified SBQLap entitys per each SBQChrono I find? Ive read a lot of posts and cant get the solution.
UPDATE:
After trying to cast the NSManagedObject, I realized that the laps is returned as nil value.
Doing:
for (NSManagedObject *oneObject in objects) {
CIChrono *chrono=(CIChrono *)oneObject;
NSLog(#"startDate %#", chrono.startDate);
NSLog(#"stopDate %#", chrono.stopDate);
NSLog(#"laps %#",chrono.laps);
}
I get the message:
2014-01-28 14:39:48.379 Chrono[2341:70b] startDate 2014-01-28 12:27:53 +0000
2014-01-28 14:39:48.380 Chrono[2341:70b] stopDate 2014-01-28 12:27:54 +0000
2014-01-28 14:39:48.380 Chrono[2341:70b] -[NSManagedObject laps]: unrecognized selector sent to instance 0x8b959b0
2014-01-28 14:39:48.383 Chrono[2341:70b] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSManagedObject laps]: unrecognized selector sent to instance 0x8b959b0'
Thanks
Lots of interesting answers on this one.
The answer is very simple and does not require subclassing NSManagedObject to access the objects in a relationship. You just need to use KVC (Key Value Coding) to access those objects. This is true of relationships and attributes.
If you want to use synthesized methods you can by subclassing as some have hinted at but that is not necessary.
Here is an extension to your example that you posted in your question:
for (NSManagedObject *oneObject in objects) {
CIChrono *chrono=(CIChrono *)oneObject;
NSLog(#"startDate %#", [chrono valueForKey:#"startDate");
NSLog(#"stopDate %#", [chrono valueForKey:#"stopDate"]);
NSLog(#"laps %#", [chrono valueForKey:#"laps"]);
NSSet *laps = [chrono valueForKey:#"laps"];
for (NSManagedObject *lap in laps) {
NSLog(#"Lap entity: %#", lap);
}
}
Note the -valueForKey: calls. Those are Key Value Coding which NSManagedObject instances (and all other Objective-C objects) respond to. I would suggest reading the documentation on KVC and KVO.
Each SQBChrono object contains a set of SBQLap objects that are associated with it. Look at your SQBChrono properties
#property (strong, nonatomic) NSOrderedSet *laps;
laps is a property that contains the many SBQLap objects to one SQBChrono object.
You can get an array from the set like this:
NSFetchRequest *request=[[NSFetchRequest alloc] initWithEntityName:kChronoEntityName];
NSError *error;
NSArray *chronoObjectArray = [appContext.managedObjectContext executeFetchRequest:request error:&error];
for (SQBChrono * chrono in chronoObjectArray) {
NSArray *lapsArray = [chrono.laps array];
NSLog("Chrono: %# laps: %#", chrono.startDate, lapsArray);
}
As you've added the SBQChrono objects to an array you will need to cast it:
SQBChrono *anObject = (SQBChrono *)objects[0];
Then you can:
anObject.laps;
Will give you the NSSet of all associated objects ?
Your array objects contains all your SBQChrono objects.
You can get all the SBQLaps associated with one chrono by doing:
SQBChrono * myChrono = objects[indexOfTheDesiredChrono];
The laps of the chrono are then in myChrono.laps, which is a NSOrderedSet (collection) of SBQLaps. Can be turn into an array like this :
NSArray * myChronoLaps = [myChrono.laps array]
You can fetch objects which have a relationship to 'SBQChrono'.
This can be done with two fetch requests, first fetch all the 'SBQChrono' objects & then perform a second request for 'SBQLap' entities with a predicate:
[NSPredicate predicateWithFormat:#"chrono IN %#",objects]
Looking at the model screenshot and code that you posted, it appears that the model has the relationship name set as lap (singular) but your class definition is using laps (plural). Core Data would be implementing an accessor for lap, but doesn't understand laps since it doesn't match the name in the model. Changing the model to say laps should fix that problem.

NSManagedObject fault / cannot get 'primitive' object handled by NSManagedObject

I am struggling with CoreData. I'd like to fetch the following object:
#interface Email : NSManagedObject {
}
#property (nonatomic, copy) NSString *email;
#property (nonatomic, copy) NSString *contact;
#end
..and put the result inside a NSMutableArray, but the NSString contents, (not NSManagedObjects!). This is because I am using json-framework and that engine does not allow NSManagedObjects to be passed.
These lines fetch perfom the fetch from CoreData
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Emails" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setPropertiesToFetch:[NSArray arrayWithObject:#"email"]];
NSError *error = nil;
NSMutableArray *fetchedObjects = [[NSMutableArray alloc] init];
fetchedObjects= [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
flight.emails=fetchedObjects;
The 'flight' object is declared as follows:
#interface Flight : NSObject {
NSMutableArray *_emails;
}
#property (nonatomic, retain) NSMutableArray *emails;
With this code I am getting CoreData faults. I tried some other implementation variants, but never actually managed to have NSString objects in flight.emails, but always NSManagedObjects. What I tried was to implement a dedicated getter function in the Email NSManagedObject that copies the fetched NSString and returns the copy.
I get the idea that this is kind of a common problem, however, my research has not led to a solution on this one here.
Thanks,
Peter
From experience, setPropertiesToFetch: only works when requesting the returned objects be dictionaries. So a couple of problems in your code:
You are asking for specific property (email), but have not set the return type to dictionary. Take a look at setResultType:.
You still need to take the result and extract the email objects from it. You cannot just assign the resulting array to your emails property.
Try this:
[fetchedRequest setResultType:NSDictionaryResultType];
NSArray* results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
if(error != nil || results == nil) return;
flight.emails = [results valueForKeyPath:#"email"];

Dynamic properties for NSManagedObjects are null after fetching

I have a problem with Core Data in my iOS app.
First off, the code for my ManagedObject:
#interface MyBook : NSManagedObject
#property (retain, nonatomic) NSString *book_name;
#end
#implementation MyBook
#synthesize book_name;
#end
And the code sample:
NSFetchRequest *fr = [NSFetchRequest fetchRequestWithEntityName:#"MyBook"];
fr.predicate = [NSPredicate predicateWithFormat:#"book_name == %#", #"Something"];
NSArray *result = [s.managedObjectContext executeFetchRequest:fr error:&error];
MyBook *book = nil;
if (result.count) {
book = [result objectAtIndex:0];
NSLog(#"Updating old book: %#", book);
} else {
book = [NSEntityDescription insertNewObjectForEntityForName:#"Book"
inManagedObjectContext:s.managedObjectContext];
NSLog(#"Creating new book");
shelf.book_name = // some unique book name is set here
}
NSLog(#"result: %#", book.book_name);
The code basically...
Tries to look up a book by name and grab that.
If the book does not exist yet, create one and set the book name.
Otherwise, grab the existing book.
The weird thing I'm experiencing is that when the book already exists, the property book_name is null. If the object was created, book_name will have the string I set before.
I guess access to the property is not causing a faulting on the fetched book instance. That's probably why [book valueForKey:#"book_name"] works and after that the property has been populated as well.
What am I missing here? I'm expecting Core Data to notice that I'm accessing the property and internally grabbing the data for me transparently.
Thank you
Big WHOOPS!
Just after posting this original question, I realized that I was using #synthesize instead of #dynamic.
However, I'll leave this question here, if anyone else experiences such an effect in the future. :)

Delete an object in core data

I have an entity in my core data model like this:
#interface Selection : NSManagedObject
#property (nonatomic, retain) NSString * book_id;
#property (nonatomic, retain) NSString * contenu;
#property (nonatomic, retain) NSNumber * page_id;
#property (nonatomic, retain) NSNumber * nbrOfOccurences;
#property (nonatomic, retain) NSString * next;
#property (nonatomic, retain) NSString * previous;
I have created many Selections and saved them in Core Data and now I would like to delete some selections with some criteria. For example, I would like to delete a Selection object if matches the following:
content = test
page_id = 5
book_id = 1331313
How I can do this?
What Mike Weller wrote is right. I'll expand the answer a little bit.
First you need to create a NSFetchRequest like the following:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:[NSEntityDescription entityForName:#"Selection" inManagedObjectContext:context]];
Then you have to set the predicate for that request like the following:
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"content == %# AND page_id == %# AND book_id == %#", contentVal, pageVal, bookVal]];
where
NSString* contentVal = #"test";
NSNumber* pageVal = [NSNumber numberWithInt:5];
NSString* bookVal = #"1331313";
I'm using %# since I'm supposing you are using objects and not scalar values.
Now you perform a fetch in the context with the previous request:
NSError* error = nil;
NSArray* results = [context executeFetchRequest:fetchRequest error:&error];
results contains all the managed objects that match that predicate.
Finally you could grab the objects and call a deletion on them.
[context deleteObject:currentObj];
Once done you need to save the context as per the documentation.
Just as a new object is not saved to the store until the context is saved, a deleted object is not removed from the store until the context is saved.
Hence
NSError* error = nil;
[context save:&error];
Note that save method returns a bool value. So you can use an approach like the following or display an alert to the user. Source NSManagedObjectContext save error.
NSError *error = nil;
if ([context save:&error] == NO) {
NSAssert(NO, #"Save should not fail\n%#", [error localizedDescription]);
abort();
}
You should perform a fetch request using an NSPredicate with the appropriate conditions, and then call the deleteObject: method on NSManagedObjectContext with each object in the result set.
In addition to Mike Weller and flexaddicted, after calling [context deleteObject:currentObj]; you need to save: context:
NSError *error = nil;
[context save:&error];
As from documentation:
Just as a new object is not saved to the store until the context is saved, a deleted object is not removed from the store until the context is saved.
That made matter in my case.

Resources