I'm developing an application that have views with a lot of fields. I have been manipulating them one by one because I don't know how to reference class attributes to be able to dynamically save them.
Right now I have a lot of line of codes to manipulate a single field... For example:
self.rowField.delegate = self;
NSArray *collectionNames = #[#"personTypeCategories", #"personTypes", #"genders", #"rows", #"seats", #"seatingOthers", #"restraintSystems", #"helmetUses"];
if ([collectionName isEqualToString:#"personTypeCategories"]) {
[self loadDefaultForCollection:#"personTypeCategories" toField:self.personTypeCategoryField withKey:#"PersonTypeCategoryID" defaultValue:self.editingPerson.typeCategoryKey];
}
if (textField == self.rowField) {
[self showCollection:#"rows" withIDColumn:#"RowID" withField:textField];
return NO;
}
When I have a new field that follow our standard of saving information and later posting it to the server I need to create this lines for each one and now I want to do it dynamically by creating an array like this:
- (void)loadViewConfiguration {
self.viewElements = [[NSMutableArray alloc] init];
[self.viewElements addObject:#{
#"restMethod" : #"personTypeCategories",
#"key" : #"PersonTypeCategoryID",
#"field" : self.personTypeCategoryField,
#"enabled" : #YES,
#"modelAttr" : &self.editingPerson.typeCategoryKey
}];
}
The problem is in the modelAttr, I need to have that class attribute reference in order to later go and modify or get it.
Another option could be if I can dynamically call class attributes like you can do in PHP, for example:
$attr = "name";
$editingPerson->{$attr} = "Omar";
Thanks for the help.
I didn't know you can use key-value coding for this purpose.
To accomplish this what I did is to simply save the name of the attribute and later get the value by using:
[classInstance valueForKey:elem[#"modelAttr"]];
An example of this:
for (NSDictionary *elem in self.viewElements) {
if ([collectionName isEqualToString:elem[#"restMethod"]]) {
[self loadDefaultForCollection:elem[#"restMethod"] toField:elem[#"field"] withKey:elem[#"key"] defaultValue:[self.editingPerson valueForKey:elem[#"modelAttr"]]];
}
}
Thanks for the advice by #matt
Related
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
main principles:
object could be created through class method by providing unique identifier (whatever)
if object with given identifier doesn't exists, returned new object otherwise returned existing one
class guarantees that ONLY ONE object with given identifier could exist (sort of internal singleton)
So main point in keeping objects with unique filed (f.e id) for future using, since they might have own states (f.e loading, loaded so on) we are allowed to use it everywhere we need it without re-creating.
Is it design pattern?
F.e:
Advirtisement.h
#interface Advertisment : NSObject
+ (instancetype)adWithID:(NSString *)adID;
+ (NSMutableArray *)sharedAds;
Advertisement.m
+ (instancetype)adWithID:(NSString *)adID {
NSMutableArray *ads = [[self class] sharedAds];
// Look for existing ad based on the id
Advertisement *returnableAd = nil;
for (Advertisement *currentAd in ads) {
if ([currentAd.adID isEqualToString:adID]) {
returnableAd = currentAd;
break;
}
}
// Create a new ad instance for this id if one doesn't already exist.
if (!returnableAd) {
returnableAd = [[[self class] alloc] initWithID:adID];
[ads addObject:returnableAd];
}
return returnableAd;
}
}
+ (NSMutableArray *)sharedAds
{
static NSMutableArray *sharedAds;
if (!sharedAds) {
sharedAds = [NSMutableArray array];
}
return sharedAds;
}
It's Lazy Initialization - and some would also consider it an instance of a Factory Pattern (others would argue that in itself a factory doesn't constitute a pattern)
While using Mantle, is there a possibility to before returning the object we are creating (on this case via JSON) to verify that X and Y properties are not nil?
Imagine this class:
#interface Person : MTLModel <MTLJSONSerializing>
#property(nonatomic,strong,readonly)NSString *name;
#property(nonatomic,strong,readonly)NSString *age;
#end
I want a way to verify that if the JSON I received doesn't have the name (for some reason there was an issue on server's DB) I will return a nil Person, as it doesn't make sense to create that object without that property set.
Whilst you could override the initialiser. It seems more concise to override the validate: as this is called at the last stage before Mantle returns the deserialised object. Makes sense to put all your validation logic in a validate method...
See the final line of the MTLJSONAdapter
id model = [self.modelClass modelWithDictionary:dictionaryValue error:error];
return [model validate:error] ? model : nil;
This tells us that if our custom model returns NO from validate, then Mantle will discard the object.
So you could in your subclass simply perform the following:
- (BOOL)validate:(NSError **)error {
return [super validate:error] && self.name.length > 0;
}
Ideally in your own implementation you probably want to return an appropriate error.
The validate method will then call Foundation's validateValue:forKey:error: for every property that you registered with Mantle in JSONKeyPathsByPropertyKey. So if you want a more controlled validation setup you could also validate your data here..
You can use the MTLJSONSerializing protocol method classForParsingJSONDictionary: to return nil rather than an invalid object:
// In your MTLModelSubclass.m
//
+ (Class)classForParsingJSONDictionary:(NSDictionary *)JSONDictionary {
if (JSONDictionary[#"name"] == nil || JSONDictionary[#"age"] == nil) {
return nil;
}
return self.class;
}
In fact I don't use Mantle, but for validation I use another GitHub Library called RPJSONValidator
It tells you the type that you expect and what type the value has been arrived.
A simple example code
NSError *error;
[RPJSONValidator validateValuesFrom:json
withRequirements:#{
#"phoneNumber" : [RPValidatorPredicate.isString lengthIsGreaterThanOrEqualTo:#7],
#"name" : RPValidatorPredicate.isString,
#"age" : RPValidatorPredicate.isNumber.isOptional,
#"weight" : RPValidatorPredicate.isString,
#"ssn" : RPValidatorPredicate.isNull,
#"height" : RPValidatorPredicate.isString,
#"children" : RPValidatorPredicate.isArray,
#"parents" : [RPValidatorPredicate.isArray lengthIsGreaterThan:#1]
} error:&error];
if(error) {
NSLog(#"%#", [RPJSONValidator prettyStringGivenRPJSONValidatorError:error]);
} else {
NSLog(#"Woohoo, no errors!");
}
Each key-value pair describes requirements for each JSON value. For example, the key-value pair #"name" : RPValidatorPredicate.isString will place a requirement on the JSON value with key "name" to be an NSString. We can also chain requirements. For example, #"age" : RPValidatorPredicate.isNumber.isOptional will place a requirement on the value of "age" to be an NSNumber, but only if it exists in the JSON.
I use a very old version of Mantle. YMMV
You can override the [MTLModel modelWithExternalRepresentation] selector. Make sure to call [super modelWithExternalRepresentation] and then add your own code to check for validate data.
I followed a small issue opened on Mantle:
- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError *__autoreleasing *)error
{
BOOL isValid = NO;
if (self = [super initWithDictionary:dictionaryValue error:error])
{
isValid = ...
}
return isValid?self:nil;
}
So in the end just override:
- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError *__autoreleasing *)error
In my CoreData model I have an Entity Article with 20 properties, and a child entity Variant, with a to-one relationship "master" to Article. So Variant has all attributes of Article parent class, but I want that if an attribute is nil, the getter should return the one of the master Article.
In code:
- (NSString *)getSomeAttribute {
NSString *tmp = self.someAttribute;
if (tmp == nil)
tmp = self.master.someAttribute;
return tmp;
}
I don't want to write 20 getters like this, is there a way to write a "general" getter for all attributes at once?
You can't skip the getters, because if you don't implement them, Core Data will generate them. What you can do is factor all of the custom code into one core method and have your other getters call that. You still end up with a bunch of getters, but they're one-liners. Something like this (warning, typed into a web browser and not compiled):
- (id)valueForKeyFromSelfOrMaster:(NSString *)key
{
[self willAccessValueForKey:key];
id value = [self primitiveValueForKey:key];
[self didAccessValueForKey:key];
if (value == nil) {
value = [self.master valueForKey:key];
}
return value;
}
- (NSString *)someAttribute
{
return [self valueForKeyFromSelfOrMaster:#"someAttribute"];
}
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.