iOS KVC DRY mutation - objective-c

I'm trying to map a dictionary of strings from a JSON fetch to a KVC compliant NSManagedObject, I can successfully use setValue: forKey: but i fail to see how I can map types.
For example I shouldn't be able to set a date to any random string: Printing description of myDate:
asdfsadf
however it worked.
I had a look at https://stackoverflow.com/a/5345023/828859 which provided some useful answers. I can go in and create validation for every single property... but that doesn't seem very DRY because ill have to validate every date and set the out value separately each time i have a date.
I would prefer to mutate by type before I use setValue: forKey: but I don't know how to discriminate on the property type.
What I want to do:
switch([object typeforkey:key]){
case #"NSDate":
//...
value = mutatedDate
//...
}
[object setValue:value forKey:key];

You can ask an object what kind of class it has been instantiated as. So you can do something like:
id myObject = [myDictionary objectForKey:key];
if ([myObject isKindOfClass:[NSDate class]]) {
// Do stuff
}
else if ([myObject isKindOfClass:[NSString class]]) {
// Do other stuff
}
This is because objects are structs containing a pointer with the ivar name isa pointing to an object of type Class, so you can always ask an object what kind of class it comes from.

I ended up using another dictionary for property type mapping. Then a object mapping object checks the object to be map abides by this particular protocol and uses the property type dictionary to convert each property before using setValue:forKey:.

Related

Objective C - Override setter to accept objects of a different type

I'm trying to override the setter of an NSManagedObject so that I can pass in an object of a different type, do a transformation and then set the property. Something like this:
- (void)setContentData:(NSData *)contentData
{
NSString *base64String;
// do some stuff to convert data to base64-encoded string
// ...
[self willChangeValueForKey:#"contentData"];
[self setPrimitiveValue:base64String forKey:#"contentData"];
[self didChangeValueForKey:#"contentData"];
}
So, in this case the contentData field of my NSManagedObject is an NSString *, and I want to allow the setter to accept an NSData * which I would then convert to an NSString * and save it to the model. However, if I try to do this I get warnings from the compiler about trying to assign an NSData * to an NSString *:
myObject.contentData = someNSData;
-> Incompatible pointer types assigning to 'NSString *' from 'NSData *__strong'
Is there a better way to go about this, or perhaps I should avoid the setters altogether and create custom "setters" that allow me to pass in the NSData * and set the NSString * field without a compiler warning?
I think this is an instance where your fighting with the tools and frameworks is a significant design smell. Retreat from this notion of trying to override the expected data type of a fundamental property for your class.
You didn't say whether the NSManagedObject you are subclassing is under your control. If it's going to be part of your design to have it be something of a template for management of other types of contentData than NSString, then declare it as type id in the root class and specialize in the subclasses. That should prevent the warning.
Probably, you want to follow a Cocoaism: don't subclass. Can you achieve whatever functionality you're looking for from the superclass by say extracting it into a helper class that is held as a property by each of the varying-behavior managed object classes?
following up on my "setContentData: (id) contentData" comment, try something like this:
- (void)setContentData:(id)thingToWorkWith
{
NSString * base64String = nil;
if(thingToWorkWith isKindOfClass: [NSData class])
{
// convert data to string
}
if(thingToWorkWith isKindOfClass: [NSString class])
{
// set up base64 string properly
}
if(base64String)
{
// do some stuff to convert data to base64-encoded string
// ...
[self willChangeValueForKey:#"contentData"];
[self setPrimitiveValue:base64String forKey:#"contentData"];
[self didChangeValueForKey:#"contentData"];
}
}
Make sure to get rid of the "#synthesize" bit for contentData in your .m file, create a "getter" method as well, and because you're using "id" for the setter parameter, you may have to adjust your "#property" declaration a bit. I haven't tried exactly what you are attempting to do (i.e. no warranties on this technique).

Objective-C - Factory to return a given class type?

With generics on languages like C# or Java, you can have a factory that returns a result depending on the given type? For example you can tell the factory method to return:
Book
List<Book>
Door
List<Door>
Is it possible to achieve the same thing with objective-c?
Can I somehow tell generateObjects method to return me an array of books?
[self getDataFromWeb:#"SOME_URL" andReturnResultWithType:[Book class]];
// What about an array of Books?
- (id)getDataFromWeb:(NSString*)url andReturnResultWithType:(Class)class
{
// Convert JSON and return result
// Mapping conversion is a class method under each contract (Book, Door, etc)
}
Let's say this is one of my data contracts
#interface Book : JSONContract
#end
#implementation Book
+ (NSDictionary *)dataMapping
{
// returns an NSDictionary with key values
// key values define how JSON is converted to Objects
}
#end
EDIT:
Modified the examples to be more clear
No, it is no possible to say that your array will contain String
But, Yes, it is possible to create String based on a Class definition or even a class name.
Objective-C as "reflection" capabilities like Java, it is called "introspection"
For example, you can create an object based on its class name using this code
NSString* myString = (NSString*)[[NSClassFromString(#"NSString") alloc] init];
NSClassFromString is documented here :
https://developer.apple.com/library/mac/#documentation/cocoa/reference/foundation/miscellaneous/foundation_functions/reference/reference.html
If you want the compiler to check types for you, you can also directly use the Class object, as this
Class stringClass = [NSString class];
NSString* myString = [[stringClass alloc] init];
Yes, NSArray and NSMutableArray store objects of type id, which means you can put whatever you want in there and return it to the user. You just check the parameter passed in to branch your logic for generating the objects you are putting in the array.
Your comment suggests this is for converting JSON? To convert JSON you must have a series of conditions checking if the value looks like a number, string, etc. So you could add a condition that says if the class parameter is NSString class then just assume the JSON value is a string.

Objective-C: override dynamic getter

I have an NSManagedObject subclass MyClass with a property myProp, which is defined #dynamic. There are various instances of reading myProp in my code, via [myClass myProp].
Now, I want to define a getter (that returns myProp after appending something to it) for myProp, without changing the various calls to [myClass myProp]. i.e. without creating a getter that is named something other than getMyProp.
My question is, if I create a getter getMyProp, which will override the getter created by NSManagedObject, how do I access the original value that is stored in the database?
To access the underlying values of a managed object you use the following two methods:
- (id)primitiveValueForKey:(NSString *)key
- (void)setPrimitiveValue:(id)value forKey:(NSString *)key
This is often used to convert NSNumber attributes into their 'real' type, for example a bool property:
- (BOOL)isShared
{
[self willAccessValueForKey:#"isShared"];
NSNumber *underlyingValue = [self primitiveValueForKey:#"isShared"];
[self didAccessValueForKey:#"isShared"];
return [underlyingValue boolValue];
}
The willAccessValueForKey: and didAccessValueForKey: are required by the underlying managed object class for handling faults and relationships etc.
And if you do end up writing a setter, you must also wrap the accessor in KVC methods:
- (void)setShared:(BOOL)isShared
{
NSNumber *newUnderlyingValue = [NSNumber numberWithBool:isShared];
[self willChangeValueForKey:#"isShared"];
[self setPrimitiveValue:newUnderlyingValue forKey:#"isShared"];
[self didChangeValueForKey:#"isShared"];
}
Having said this, I would personally not recommend you keep the same method name unless you have a good reason. For 'derived' values you generally want to create a brand new method with a different name. It doesn't take long to do a quick find/replace throughout your code.
EDIT: added willAccessValueForKey:/didAccessValueForKey: (thanks jrturton)

ios sdk - checking true type of a newly created nsmanagedobject's property (with value of type nsnull)

i am creating various nsmanagedobjects from nsdictionaries and while i am iterating over the object's properties i assign the corresponding values from the dictionary.
currently i check the type like this:
if ([attributeValue isKindOfClass:[NSString class]]) {
...
}
now the problem is that once the managed object is created, all (e.g. string) values do not respond to comparing classes. apparently they are not nsnull either. i miss everything but the nsnumbers.
[[attributeValue class] description]
this is empty for everything but the nsnumbers on the unset values.
is there a way to access the type of the property itself like it is defined in the class header, instead of using the (still unset) value?
Every NSManagedObject in a model has a corresponding NSEntityDescription. You can use the entity description to get the list of attributes and relationships. These will be NSAttributeDescriptions or NSRelationshipDescriptions which have in them the information you require.
I know, this is a late answer, but as I was search today for a good solution...
This is what I'm using now:
NSEntityDescription * myEntity = [object entity];
NSDictionary * attributes = [myEntity attributesByName];
if ([[[attributes objectForKey:key] attributeValueClassName] isEqualToString:#"NSDate"]) {
// this type is NSDate
}
Hope this helps :)

Cocoa: deserialize json string to custom objects (not NSDictionary, NSArray)

In java-land, there are a handful of useful libraries which will convert json strings to objects of matching type. The json libraries I've seen for cocoa simply create nested NSDictionaries and NSArrays. Is there a tool out there which will go the extra step of reconstituting whatever object type I want?
So, for example, if I have a class called "Unicorn", with a property "maneColor", and I have json that looks like this:
{
"maneColor":"silver"
}
I can automatically instantiate a Unicorn object with "maneColor" set to "silver".
I'm not aware of any specific implementations, but key-value coding gets you very close to what you want: Key Value Coding Guide. I've had good results combining streamed json parsing with KVC.
The -setValue:forKey: method makes adapting serialized data to custom objects fairly straightforward. To continue with your example, you'd create a Unicorn class with all required accessor methods: -setName:/-name, -setManeColor/-maneColor, etc. (You may be able to use properties for some expected values, but there are cases, as with the maneColor value, where you probably want to write a custom setter to convert from the color name string to an NSColor or UIColor object.)
You'll also want to add two more methods to your custom object: -setValue:forUndefinedKey: and -valueForUndefinedKey:. These are the methods that will be called if your object has no accessor methods matching a key passed into the KVC methods. You can catch unexpected or unsupported values here, and store them or ignore them as necessary.
When you send -setValue:forKey: to the Unicorn object, the framework looks for accessors matching the key pattern. For instance, if the key is "maneColor" and you're setting the value, the framework checks to see if your object implements -setManeColor:. If so, it invokes that method, passing in the value; otherwise, -setValue:forUndefinedKey: is called, and if your object doesn't implement it, an exception is thrown.
When your parser's delegate receives notification that parsing a json unicorn object has begun, instantiate a Unicorn object. As your parser returns the parsed data to you, use -setValue:forKey: to add the data to your object:
- ( void )parserDidBeginParsingDictionary: (SomeParser *)p
{
self.currentUnicorn = [ Unicorn unicorn ];
}
- ( void )parser: (SomeParser *)p didParseString: (NSString *)string
forKey: (NSString *)key
{
[ self.currentUnicorn setValue: string forKey: key ]
}
- ( void )parserDidFinishParsingDictionary: (SomeParser *)p
{
[ self.unicorns addObject: self.currentUnicorn ];
}
Use Jastor - https://github.com/elado/jastor
Takes already parsed JSON into NSDictionary and fills an instance of real Objective-C class.
NSDictionary *parsedJSON = (yajl, JSONKit etc)
Unicorn *unicorn = [[Unicorn alloc] initWithDictionary:parsedJSON];
unicorn.maneColor // "silver"
As any subclass of NSObject conforms to NSKeyValueCoding protocol:
NSDictionary *parsedJSON = //whatever
id <NSKeyValueCoding> entity = [[CustomNSObjectSubclass alloc] init];
[entity setValuesForKeysWithDictionary:parsedJSON];
Apple added the NSJSONSerialization class to iOS 5.0 which, according to the documentation, does the following:
You use the NSJSONSerialization class to convert JSON to Foundation
objects and convert Foundation objects to JSON.
An object that may be converted to JSON must have the following
properties:
The top level object is an NSArray or NSDictionary. All objects are
instances of NSString, NSNumber, NSArray, NSDictionary, or NSNull. All
dictionary keys are instances of NSString. Numbers are not NaN or
infinity.
Here's a tutorial and wrapper method to get you started.