Using Mantle with Core Data - NSSet and NSArray - ios

I need your help with something, as I can't get my head around this. I'm using Mantle together with CoreData in iOS.
I have relationships defined that look as follows:
Post 1:N Comment
When I pull the data from my REST Service, I create a Mantle Object Post that has a NSMutableArray of Comments in it. This works flawlessly.
I then store this in Core Data, and this is where I don't know whether I am doing things right.
[MTLManagedObjectAdapter managedObjectFromModel:post insertingIntoContext:[self getManagedObjectContext] error:&error];
So I'm doing this to store my post object into Core Data. The Core Data Model has a relationship called "post_has_comments" which is a cascading One-to-Many relationship. So on the object Post I have "posts_has_comments" -> cascading, on my object "Comment" I have a one-to-one relationship with "Nullify".
Afaik, Core Data treats this as a NSSet. What I'm trying to put in is a NSMutableArray though, as Mantle will take care of this (at least thats what a quick look in its source told me).
Unfortunately, when I get the object back from Core Data with
Post* post = [MTLManagedObjectAdapter modelOfClass:Post.class fromManagedObject:[[self fetchedResultsController] objectAtIndexPath:indexPath] error:nil];
The property comments on the post object is a empty NSSet, and I get quite some errors upon inserting the thing beforehand. The errors I get:
Core Data: annotation: repairing missing delete propagation for to-many relationship post_has_comments on object [...]
I am stuck - Maybe I am missing something huge here?
My Post Class implements the following static methods:
+ (NSDictionary *)managedObjectKeysByPropertyKey {
return #{
#"post_id" : #"id",
#"comments" : #"post_has_comments"
};
}
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return #{
#"post_id" : #"id",
};
}
+ (NSDictionary *)relationshipModelClassesByPropertyKey {
return #{
#"comments" : IHComment.class
};
}

A simple workaround is to write your own property setter method and if value being set is NSSet then convert it to NSMutableArray before setting it back to your property ivar.
For example:
- (void)setComments:(NSMutableArray *)comments {
if ([comments isKindOfClass:NSSet.class]) {
_comments = [NSMutableArray arrayWithArray:[((NSSet *)comments) allObjects]];
} else {
_comments = comments;
}
}
I've done it myself quite a few times and it works like a charm!

From the Mantle documentation:
Mantle makes it easy to write a simple model layer for your Cocoa or Cocoa Touch application.
This is simply an unproven statement. Looking at the framework, I do not see where the evidence is. You should get your objects, and insert them into Core Data using Apple's APIs.
Post *cdPost = [NSEntityDescription insertNewObjectForEntityForName:#"Post"
inManagedObjectContext:self.managedObjectContext];
// configure the cdPost object with the data from the web service
for (id commentObject in commentArrayFromPostObject) {
Comment *cdComment =
[NSEntityDescription insertNewObjectForEntityForName:#"Comment"
inManagedObjectContext:self.managedObjectContext];
// configure the cdComment object with the data from the web service
cdComment.post = cdPost;
}
That's all there is to it.

Related

How to handle unexpected data types with Mantle

In my MTLModel subclass I have this:
#property (assign, nonatomic) NSInteger catId;
And of course this in the implementation:
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return #{
#"catId" : #"cat_id"
};
}
But what if my server friends decide to change cat_id to a string in the JSON response? How can I handle this case, and convert it to an int so that I don't get Mantle errors?
We also used Mantle for quite some time, but at the end migrated to handwritten parser/serializers, as the task itself seem to be trivial.
Though, we also have this kind of problems: what if server return something we do not expect (e.g. NSDictionary instead of NSString).
To prevent our app from crashing we use this simple tool: Fuzzer.
Basically the tool provides a method that takes a sample and a block. The block evaluates several times, each time bringing slightly mutated version of the sample. You can check behaviour of the models/parsers/serializers using the mutants, to ensure that your code handles unexpected data gracefully.
Here is example taken from project's README:
- (void)test {
NSDictionary *sample = #{
#“name” : #“John Doe”,
#“age” : #42
};
UserDeserializer *deserializer = [UserDeserializer new];
FZRRunner *runner = [FZRRunner runnerWithBuiltinMutationsForSample:sample];
NSArray *reports = [runner enumerateMutantsUsingBlock:^(NSDictionary *mutant) {
[deserializer deserializeUser:mutant];
}];
XCTAssertEqual(reports.count, 0);
}
if([obj isKindOfClass:NSClassFromString(#"__NSCFNumber")])
{
//if it is int or number
}
else
{
}
May be above method will help you

Using Core Data Transformations for Encryption

I'm stuck on why my transformable classes aren't being called. The following screenshot shows my Entity attribute as a transformable:
According to the documentation, it should automatically call the class "StringEncryptionTransformer" and perform the transformation.
I followed this guide in order to set up the class. I'm using the EncryptionTransformer and StringEncryptionTransformer classes provided, the only change I made was with the encryption to use RNcryptor.
Encryption:
return [RNEncryptor encryptData:data withSettings:kRNCryptorAES256Settings password:[self key] error:&error];
and Decryption:
return [RNDecryptor decryptData:data withPassword:[self key] error:&error];
The saved entity appears never to go through the transformation, is there something I'm missing? I tried adding an initialize to the NSManagedObject, but the results were the same.
you need to register value transformer like below
extension NSValueTransformerName {
static let classNameTransformerName = NSValueTransformerName(rawValue: "ClassNameTransformer")
}
ValueTransformer.setValueTransformer(ClassNameTransformer(), forName: .classNameTransformerName)

Custom NSManagedObject subclass constructor

I have a question about CoreData and NSManagedObject.
In the architecture of my application I have a REST client That builds instances of objects That I receive from the server.
For example I have this method:
- (NSArray*) getAllCards:(NSDictionary*) jsonResponse;
That takes the JSON response and returns an array of Cards.
It's ok ...
Now I need to save my Card in CoreData.
I defined my model so I have some subclass of NSManagedObject.
For example: Card: NSManagedObject
but I would like to have a simple constructor to construct the object from the JSON and then pass this to my manager who will add it in CoreData.
I would not pass some info related to CoreData (eg. Context ..) to my Client REST ... I would like to have the logic in my CoreData CoreData Manager ..
How can I define a valid constructor of my Cards?
Can I define a constructor in this way?
- (id) initWithName:(NSString*) name
cardId:(NSString*) cardId
type:(NSString*) type
{
self = [super initWithEntity:nil insertIntoManagedObjectContext:nil]; //This is wrong..
if (self) {
self.name = name;
self.cardId = cardId;
self.type = type;
}
}
I see no need to override the init methods. Just give your CoreDataManager class a method that takes the dictionary and generates the core data entities.
-(void)storeCardsFromJSON:(NSDictionary*)dictionary {
for (NSDictionary *cardInfo in dictionary[#"cardArray"]) {
Card *newCard = [NSEntityDescription insert...];
newCard.cardID = cardInfo[#"cardID"];
// etc.
}
// save
}
Optionally, return an array of newly created cards. In this method you could also filter out the cards that already exist (e.g. based on the cardID attribute) and modify those instead of inserting them again.
In your app, you should only use the Card objects from Core Data. There is no need to have a different class of objects, or use NSDictionary objects.
Maybe it will help: for all of my NSManagedObject subclasses, that can be created from JSON, I added a category with names like MyObject+JSON and I have 2 methods there:
+ (instancetype)deserializeFromDictionary:(NSDictionary *)dictionary inContext:(NSManagedObjectContext *)context;
- (void)updateFromDictionary:(NSDictionary *)dictionary;
I use inContext: part because I usually insert objects in batches using private context for the whole transaction.
Methods look like this:
+ (instancetype)deserializeFromDictionary:(NSDictionary *)dictionary inContext:(NSManagedObjectContext *)context;
{
if (!dictionary || ![dictionary isKindOfClass:[NSDictionary class]] || !context) {
return nil;
}
MyObject *anObject = (MyObject *)[NSEntityDescription insertNewObjectForEntityForName:#"MyObject" inManagedObjectContext:context];
[anObject updateFromDictionary:dictionary];
return anObject;
}
And
- (void)updateFromDictionary:(NSDictionary *)dictionary
{
NSParameterAssert(dictionary);
if (!dictionary || ![dictionary isKindOfClass:[NSDictionary class]]) {
return;
}
self.identifier = dictionary[#"id"];
self.title = dictionary[#"title"]; //and so on
}

iOS Mantle + Overcoat

I'm looking at the Overcoat library which from what I gather is a library that extends the Mantle library.
Mantle: https://github.com/Mantle/Mantle/
Overcoat: https://github.com/gonzalezreal/Overcoat
The Mantle and Overcoat github pages keeps mentioning about creating a Mantle model but I want to know how do I generate the Mantle model? Do I manually type it out or do I use Xcode xcdatamodel file to build it visually, then generate the sublass + modify that file afterwards?
In Core Data do create the entity in the xcdatamodel file using the Interface Builder, then use Xcode's Editor > Create NSManagedObject subclass.
Do we do the same for Mantle and then change from NSManagedObject to MTLModel ?
What happens when we decided to update the Core Data entity in the xcdatamodel file? If we regenerate the model file again, wouldn't we have to re-add all those changes to the NSManagedObject class?
Super confused about the process.
OK, I think I'm starting to grasp it a bit more. After a couple of hours of trial and error, I was able to get a basic Overcoat demo app working with Core Data pulling from my REST API.
I got it to work like so:
1) We create the Entities inside the xcdatamodel file BUT DO NOT generate the NSManagedObject class using Editor > Create NSManagedObject Classes menu.
2) Create the Mantle model subclass per the normal Right Click project folder (in Xcode) > New File > Choose MTLModel as subclass and then manually entering in the properties. Notably, the subclass header should be something like:
#interface Book : MTLModel <MTLJSONSerializing, MTLManagedObjectSerializing>
#property (nonatomic, copy) NSString *title;
...
#end
3) If you accidentally generated the Core Data entity like me, the xcdatamodel file actually adds the class name in the "Default" section under "Configuration" inside the xcdatamodel.
You need to delete any value in the "Class" column otherwise you end up with a bad crash saying:
"XYZ" is not a subclass of NSManagedObject.
4) Ensure in your Mantle model class you implement the serializing methods for MTLJSONSerialization and MTLManagedObjectSerializing.
#pragma mark - MTLJSONSerialization -
+(NSDictionary *)JSONKeyPathsByPropertyKey
{
return #{
#"title": #"title",
...
};
}
#pragma mark - MTLManagedObjectSerializing -
+(NSString *)managedObjectEntityName
{
// ------------------------------------------------
// If you have a Core Data entity called "Book"
// then you return #"Book";
//
// Don't return the Mantle model class name here.
// ------------------------------------------------
return #"TheCoreDataEntityName";
}
+(NSDictionary *)managedObjectKeysByPropertyKey
{
// ------------------------------------------------
// not really sure what this does, I just put
// it in as the example does it too
// ------------------------------------------------
return #{};
}
These methods essentially is the glue mapping the JSON response from the server to the Core Data Entities.
5) One more important thing that got me is the way the server return responses.
Your server might use HTTP Status code and no top level JSON dictionary
e.g.
// no top level JSON dictionary, purely just an array of results
{
{
title: "ABC",
...
},
{
title: "ABC",
...
},
{
title: "ABC",
...
},
}
Whereas, other types of REST server might return a top level JSON dictionary with the results key path at a sub level like so:
{
count: 20,
next: "http://www.server.com/api/resource?page=2",
previous: null,
results:(
{
title: "ABC",
...
},
{
title: "ABC",
...
},
{
title: "ABC",
...
})
}
In the latter case, that's known as an "Envelop" type of response from my vague understanding. For these type of server responses, there is an extra step that involves telling Overcoat where the array of results key path is in the JSON response.
To do this, we need to:
5a) Create a ServerResponse class that is a subclass of OVCresponse:
// .h file
#import "OVCResponse.h"
#interface ServerResponse : OVCResponse
#end
// .m file
#implementation ServerResponse
+(NSString *)resultKeyPathForJSONDictionary:(NSDictionary *)JSONDictionary
{
// --------------------------------------------------------------------
// we're telling Overcoat, the array of entities is found under the
// "results" key-value pair in the server response JSON dictionary
// --------------------------------------------------------------------
return #"results";
}
#end
5b) In your APIClient class (which should be a subclass of OVCHTTPSessionManager), override the method:
+(Class)responseClass
{
// --------------------------------------------------
// ServerResponse class will let Overcoat know
// where to find the results array
// --------------------------------------------------
return [ServerResponse class];
}
Hopefully this helps anyone else who's having the same problem trying to get Overcoat working.

RestKit 0.09: mapping JSON array of strings

My question is very similar to RestKit: mapping JSON array of strings, except it is specific to RestKit 0.09. I have looked at several other questions but am still stuck.
For reasons beyond my control I cannot upgrade to RestKit 0.20. The JSON I'm working with is:
{
"brands":[
"AN",
"UO",
"FP",
"TR",
"BN"
],
"meta":{
"message":"Current Registry Configuration."
},
"event_types":[
"WEDDING",
"ANNIVERSARY",
"BIRTHDAY",
"HOUSEWARMING",
"SPECIAL OCCASION"
],
"links":[
]
}
I am able to map the "meta" (and several other domain) objects just fine. But I have not been able to map "event_types", and have no need for "brands" or "links".
My current code is as follows:
+ (void) addElementMappings:(RKObjectMapping *)mapping
{
if ([[self superclass] respondsToSelector:#selector(addElementMappings:)]) {
[[self superclass] performSelector:#selector(addElementMappings:)
withObject:mapping];
}
[mapping mapKeyPath:#"event_types"
toAttribute:#"eventType"];
RKObjectManager* objectManager = [RKObjectManager sharedManager];
if (![objectManager.mappingProvider mappingForKeyPath:#"event_types"]) {
[objectManager.mappingProvider setMapping:mapping
forKeyPath:#"event_types"]; // for KVC path
}
}
and eventType is a NSArray (I've also tried defining it as NSString - same results as below).
As is, the code throws an exception "this class is not key value coding-compliant for the key event_types."
If I change the mapKeyPath from #"event_types" to nil (similar to 0.20), I get an exception "Cannot define an element mapping an element name to map from".
If I omit the [mapping mapKeyPath:toAttribute] entirely, there are no exceptions but of course I only get the meta object, not event_types.
In RestKit 0.09, how do I map attributes without keys?
The solution was to create a class (GRConfigDTO) that contained the nested objects of interest (brands, event_types) as NSArray. The mapping was simple:
[mapping mapKeyPath:#"brands"
toAttribute:#"brands"];
[mapping mapKeyPath:#"event_types"
toAttribute:#"eventTypes"];
My predecessors had modified RestKit .09 (and our server JSON) to ensure that mapping was never ambiguous and therefore always used nil targetClass - which is why upgrading RestKit was not an option. I was working with a 3rd party service and had to deal with their JSON as-is.
The key to making it all work was specifying the targetClass in the sendObject call:
[self sendObject:nil
path:kConfig
requestType:GET
targetClass:[GRConfigDTO class]
delegate:self
callbackMethod:#selector(registryEventTypesResults:error:)];
With the targetClass set, RestKit was able to perform the mapping as expected.

Resources