Transform String to Core Data by NSValueTransformer - iphone

I'm trying to use Core Data NSValueTransformer to transform NSArray to NSString. I'm not sure whether it can be archived, but I saw apple's official doc show a code snippet:
#interface ClassNameTransformer: NSValueTransformer {}
#end
#implementation ClassNameTransformer
+ (Class)transformedValueClass { return [NSString class]; }
+ (BOOL)allowsReverseTransformation { return NO; }
- (id)transformedValue:(id)value {
return (value == nil) ? nil : NSStringFromClass([value class]);
}
#end
It seems it can store data into NSString (maybe I misunderstood..), so I tried like below:
#implementation ArrayToStringTransformer
+ (BOOL)allowsReverseTransformation {
return YES;
}
+ (Class)transformedValueClass {
return [NSString class];
}
- (id)transformedValue:(id)value {
// return NSStringFromClass([value class]);
// return NSStringFromClass([#"11" class]);
NSLog(#"!!!!!! %#, %#", NSStringFromClass([value class]), value);
if ([value isKindOfClass:[NSString class]])
return NSStringFromClass([value class]);
NSMutableString * string = [NSMutableString string];
for (NSNumber * number in value)
[string appendString:[NSString stringWithFormat:#"%#,", number]];
return string;
}
- (id)reverseTransformedValue:(id)value {
NSArray * array = [value componentsSeparatedByString:#","];
return array;
}
#end
However, it crashed with the error below (it includes the NSLog):
[26364:11903] !!!!!! __NSCFString, 0,0
[26364:11903] -[__NSCFString bytes]: unrecognized selector sent to instance 0x905f620
[26364:11903] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFString bytes]: unrecognized selector sent to instance 0x905f620'
Any idea? Please!!
EDIT:
Well, I just transform NSArray to NSData, and if it is NSString type, transform it to NSArray first, then to NSData, and it works:
+ (Class)transformedValueClass {
return [NSArray class];
}
- (id)transformedValue:(id)value {
if (! [value isKindOfClass:[NSString class]])
return [NSKeyedArchiver archivedDataWithRootObject:value];
NSLog(#"!!! Convert NSString to NSArray");
NSArray * array = [value componentsSeparatedByString:#","];
NSData * data = [NSKeyedArchiver archivedDataWithRootObject:array];
return data;
}
- (id)reverseTransformedValue:(id)value {
return [NSKeyedUnarchiver unarchiveObjectWithData:value];
}
!!! Attention Here
But I wonder whether it is a right way? As you can see, I return [NSArray class] in transformedValueClass method, but actually return NSData type data value in transformedValue:.
The apple DOC said:
An NSData object containing the encoded form of the object graph whose root object is rootObject
I'm totally confused...

Currently Core Data can only transform to NSData. The default transformer can handle transforming an array of strings to NSData by setting the attribute type to transformable and that's the only configuration required.
Personally I would like Core Data in the future to allow transformation to a string, so for example when using a SQLite browser the record field is human-readable instead of being a binary plist.
One theory why they might not have included this feature, is if you are trying to include an array of strings, then you would likely be better off using a many-to-many relation for implementing what you are trying to store instead.

Related

encodeWithCoder is not called in derived class of the NSMutableDictionary

Basically I am using some open source code called OrderedDictionary that is derived from NSMutableDictionary. Then I want to save the ordered dictionary data to NSUserDefaults by adding encode and decode method to the OrderedDictionary class. However, I realized the encode and decode methods are not being called, as a result, the decoded dictionary is no longer ordered. Below is my code:
#interface OrderedDictionary : NSMutableDictionary <NSCopying, NSCoding>
{
NSMutableDictionary *dictionary;
NSMutableArray *array;
}
In the implementation file:
/**
* encode the object
**/
- (void)encodeWithCoder:(NSCoder *)coder
{
[super encodeWithCoder:coder];
[coder encodeObject:dictionary forKey:#"dictionary"];
[coder encodeObject:array forKey:#"array"];
}
/**
* decode the object
*/
- (id)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self != nil)
{
dictionary = [coder decodeObjectForKey:#"dictionary"];
array = [coder decodeObjectForKey:#"array"];
}
return self;
}
Quick example code for using this:
dictionary = [[OrderedDictionary alloc] init];
[dictionary setObject:#"one" forKey:#"two"];
[dictionary setObject:#"what" forKey:#"what"];
[dictionary setObject:#"7" forKey:#"7"];
NSLog(#"Final contents of the dictionary: %#", dictionary);
if ([[NSUserDefaults standardUserDefaults] objectForKey:#"myDictionary"] == nil)
{
[[NSUserDefaults standardUserDefaults] setObject:[NSKeyedArchiver archivedDataWithRootObject:dictionary]
forKey:#"myDictionary"];
}
else
{
NSUserDefaults *currentDefaults = [NSUserDefaults standardUserDefaults];
NSData *savedDictionary = [currentDefaults objectForKey:#"myDictionary"];
if (savedDictionary != nil)
{
OrderedDictionary *oldData = [NSKeyedUnarchiver unarchiveObjectWithData:savedDictionary];
if (oldData != nil)
{
NSLog(#"Final contents of the retrieved: %#", oldData);
}
}
}
The thing is, the final retrievedDictionary does NOT have the original data order and the encode and decode methods are not called at all.
Thanks for any help in advance! :)
There's an NSObject method called -classForCoder that you need to override in OrderedDictionary. From the docs:
classForCoder
Overridden by subclasses to substitute a class other than its own during coding.
-(Class)classForCoder
Return Value
The class to substitute for the receiver's own class during coding.
Discussion
This method is invoked by NSCoder. NSObject’s
implementation returns the receiver’s class. The private subclasses of
a class cluster substitute the name of their public superclass when
being archived.
That last line is the key. So, in OrderedDictionary.m:
- (Class)classForCoder
{
return [self class]; // Instead of NSMutableDictionary
}
Also, if you're not using ARC, make sure you retain the objects coming back from -decodeObjectForKey. As rob mayoff mentions below, you shouldn't call [super initWithCoder:] (or [super encodeWithCoder:'). I also changed the key strings to avoid collisions.
- (id)initWithCoder:(NSCoder *)coder
{
if (self != nil)
{
dictionary = [[coder decodeObjectForKey:#"OrderedDictionary_dictionary"] retain];
array = [[coder decodeObjectForKey:#"OrderedDictionary_array"] retain];
}
return self;
}
You are possibly creating a new OrderedDictionary with the wrong initializer, initWithDictionary:. Try this instead:
OrderedDictionary *oldData = [NSKeyedUnarchiver unarchiveObjectWithData: savedDictionary];
if (oldData != nil)
{
NSLog(#"Final contents of the retrieved: %#", oldData);
}
Make sure initWithDictionary: expects OrderedDictionary as an argument. My guess is that it expects an NSDictionary.
Edit: the code to save the defaults should include something like this:
OrderedDictionary *myDict = ...;
NSData* data = [NSKeyedArchiver archivedDataWithRootObject: myDict];
[[NSUserDefaults standardDefaults] setObject: data forKey: #"myDictionary"];

Read plist stored in memory to NSDictionary

According to this post I can use the encryption/decryption methods to store/retrieve plist file securely.
But the problem is:
Q: After I have decrypted the plist file, how can I parse and store the plist file as NSDictrionary object
Probably NSPropertyListSerialization is what you are looking for.
As seen in this Post:
Plist Array to NSDictionary
You could use core foundation approach here with the method CFPropertyListCreateFromXMLData
If the plist represents the NSDictionary content, the following check should be passed:
if ([(id)plist isKindOfClass:[NSDictionary class]])
and the plist object might be safely casted to NSDictionary. If no, something is wrong with the data or decription process.
The easiest way would be creating a category for NSDictionary like this:
NSDictionaryWithData.h:
#interface NSDictionary (NSDictionaryWithData)
+ (id)dictionaryWithData:(NSData *)data;
- (id)initWithData:(NSData *)data;
#end
NSDictionaryWithData.m:
#implementation NSDictionary (NSDictionaryWithData)
+ (id)dictionaryWithData:(NSData *)data
{
return [[[NSDictionary alloc] initWithData:data] autorelease];
}
- (id)initWithData:(NSData *)data
{
self = (NSDictionary *)[NSPropertyListSerialization propertyListFromData:data
mutabilityOption:NSPropertyListImmutable
format:NULL
errorDescription:nil];
return [self retain];
}
#end
Usage:
NSDictionary* myDict = [[NSDictionary alloc] initWithData:decryptedData];
One of the possible reasons dictionaryWithData doesn't exist is a property list is not necessarily a dictionary at the root level. It could equally be an NSArray.
Here is my take on a solution: a category that utilises NSPropertyListSerialization
Features
Silently discards data that contains arrays at the root level.
Checks which method to use ( propertyListFromData:mutabilityOption:format:errorDescription: is depreciated )
NSMutableDictionary also supported
Note - this takes an unorthodox approach of wrapping a class factory method with an init method. This is for efficiency - most of the time you will be using the factory method, which just wraps NSPropertyListSerialization, which internally invokes alloc/init/autorelease to return an appropriate object.
NSDictionary+DictionaryWithData.h
#import <Foundation/Foundation.h>
#interface NSDictionary (DictionaryWithData)
+ (id)dictionaryWithData:(NSData *)data;
- (id)initWithData:(NSData *)data;
#end
NSDictionary+DictionaryWithData.m
#import "NSDictionary+DictionaryWithData.h"
#implementation NSDictionary (DictionaryWithData)
+(NSPropertyListMutabilityOptions) mutabilityOption {
return NSPropertyListImmutable;
}
+ (id)dictionaryWithData:(NSData *)data
{
static BOOL methodChecked = NO;
static BOOL use_propertyListWithData = NO;
if (!methodChecked) {
SEL sel = #selector(propertyListWithData:options:format:error:);
use_propertyListWithData = [[NSPropertyListSerialization class]
respondsToSelector:sel];
methodChecked = YES;
}
id result;
if (use_propertyListWithData) {
result = [NSPropertyListSerialization propertyListWithData:data
options:[self mutabilityOption]
format:nil
error:nil];
} else {
result = [NSPropertyListSerialization propertyListFromData:data
mutabilityOption:[self mutabilityOption]
format:NULL
errorDescription:nil];
}
return [result isKindOfClass:[NSDictionary class]] ? result : nil;
}
- (id)initWithData:(NSData *)data
{
id result = [[self class] dictionaryWithData:data];
self = result ? [self initWithDictionary:result ] : nil;
return self;
}
#end
#implementation NSMutableDictionary (DictionaryWithData)
+(NSPropertyListMutabilityOptions) mutabilityOption {
return NSPropertyListMutableContainersAndLeaves;
}
#end

NSDictionary isn't responding to objectForKey and valueForKey

I have the following
// this code is inside cellForRowAtIndexPath for a TableViewController
id answer = [self.answers objectAtIndex:indexPath.row];
if ([answer respondsToSelector:#selector(objectForKey)]) {
cell.textLabel.text = [answer valueForKey:#"answer_id"];
} else {
// I'm ending up here, instead of the cell.textLabel being set
[NSException raise:#"Answer is of invalid class" format:#"It should be able to respond to valueForKey, class: %#", [answer class]];
}
where self.answers is set to
// the question that gets passed here is a parsed single object
// from the `/questions` path
- (NSArray *)answersForQuestion:(NSDictionary *)question {
NSString *contents = [self loadContentsForPath:[question valueForKey:#"question_answers_url"]];
valueForKey:#"question_answers_url"]];
NSDictionary *data = [contents JSONValue];
NSArray *answers = [data valueForKey:#"answers"];
return answers;
}
- (NSString *)loadContentsForPath:(NSString *)path {
NSString *wholeURL = [#"http://api.stackoverflow.com/1.1" stringByAppendingString:path];
return [NSString stringWithContentsOfURL:[NSURL URLWithString:wholeURL] encoding:NSUTF8StringEncoding error:nil];
}
I'm doing exactly the same thing for loading questions which works just fine, but it seems
to fail on answers when I try to do [answers valueForKey:#"answer_id"].
I don't think this is a problem with the JSON parser, because it works fine for the /questions data.
When the debugger stops on the exception and when I try to right click -> Print Description on answers, I get
Printing description of answer:
<CFBasicHash 0x6ec1ec0 [0x1474b38]>{type = mutable dict, count = 13,
entries =>
1 : <CFString 0x6ec57d0 [0x1474b38]>{contents = "down_vote_count"} = <CFNumber 0x6e1dc00 [0x1474b38]>{value = +0, type = kCFNumberSInt32Type}
2 : <CFString 0x6ec4ee0 [0x1474b38]>{contents = "last_activity_date"} = <CFNumber 0x6ec5780 [0x1474b38]>{value = +1326379080, type = kCFNumberSInt64Type}
3 : <CFString 0x6ec44b0 [0x1474b38]>{contents = "community_owned"} = <CFBoolean 0x1474f68 [0x1474b38]>{value = false}
...
which to me seems like a regular hash. I tried both objectForKey and valueForKey and neither of them work, i.e.
exception 'NSInvalidArgumentException', reason: '-[__NSCFNumber isEqualToString:]: unrecognized selector sent to instance 0x6b2a330'
when I do just
cell.textLabel.text = [answer objectForKey:#"answer_id"];
The : is a part of the method name, so you need to do:
if ([answer respondsToSelector:#selector(objectForKey:)]) {
And then use objectForKey:, not valueForKey:. The first is to access objects in the dictionary, the later is for the so-called Key-Value Coding. So it's:
id answer = [self.answers objectAtIndex:indexPath.row];
if ([answer respondsToSelector:#selector(objectForKey:)]) {
cell.textLabel.text = [answer objectForKey:#"answer_id"];
} else {
[NSException raise:#"Answer is of invalid class" format:#"It should be able to respond to objectForKey:, class: %#", [answer class]];
}
Last but not least, it looks like the object you get out of the answer dictionary is a NSNumber, not an NSString. So you might want to change the setting of the text to:
cell.textLabel.text = [[answer objectForKey:#"answer_id"] description];
Actually, stack overflow's api sends back a number. That is to say that the value stored for the key answer_id is an NSNumber in the dictionary and not NSString and you should treat it accordingly.
you can use: --
NSDictionary *answer = [self.answers objectAtIndex:indexPath.row];
if ([answer respondsToSelector:#selector(objectForKey)]) {
cell.textLabel.text = [answer valueForKey:#"answer_id"];
} else {
[NSException raise:#"Answer is of invalid class" format:#"It should be able to respond to valueForKey, class: %#", [answer class]];
}
OR
id answer = [self.answers objectAtIndex:indexPath.row];
if (answer isKindOfClass:[NSDictionary class])
{
cell.textLabel.text = [answer valueForKey:#"answer_id"];
} else {
// I'm ending up here, instead of the cell.textLabel being set
[NSException raise:#"Answer is of invalid class" format:#"It should be able to respond to valueForKey, class: %#", [answer class]];
}

Accessing C arrays(int[], float[], etc..) using Objective-C runtime

I'm trying to save all the variables in my class into NSUserDefaults using objc/runtime. And below is the code I'm using.
NSUInteger count;
Ivar *iVars = class_copyIvarList([self class], &count);
for (NSUInteger i=0; i<count; i++)
{
Ivar var = iVars[i];
NSString *varName = [NSString stringWithCString:ivar_getName(var) encoding:NSUTF8StringEncoding];
NSString *varType = [NSString stringWithCString:ivar_getTypeEncoding(var) encoding:NSUTF8StringEncoding];
if([varType hasPrefix:#"["])
{
NSLog(#"Array");
id var1 = [_manager valueForKey:varName];
NSLog(#"--- %#", var1);
NSData *data = [NSData dataWithBytes:&([_manager valueForKey:varName]) length:sizeof([_manager valueForKey:varName])]
[[NSUserDefaults standardUserDefaults] setObject:[_manager valueForKey:varName] forKey:varName];
}
else
{
NSLog(#"NonArray");
NSLog(#"--- %#", [_manager valueForKey:varName]);
[[NSUserDefaults standardUserDefaults] setObject:[_manager valueForKey:varName] forKey:varName];
}
}
free(iVars);
The problem is that, when there are only primitive datatypes, the above code works just fine. But, when I try to access a array variable like int[], or float[], it gets crashed with SIGABRT. it is not showing any other messages.
valueForKey doesn't return any values for C arrays.
If anybody know how to load values for C-arrays in runtime, please help.
Thanks in advance,
Suran
Unless you always provide a paired length method, your program will never know the length of the array returned. So... you will need to do some work someplace to accomplish this without a crash.
If I really wanted to do what you're doing, I would make the class itself create the array, providing NSData. If this is common, you may want to use a convention:
- (int*)pixelBuffer;
- (NSData *)pixelBufferForSerialization; // << returns a deep copy of
// self.pixelBuffer as an
// NSData instance.
So your above implementation would see that the property defines a scalar array, and then request NSData * data = obj.pixelBufferForSerialization; instead of trying to produce the data itself.
Update
It's best to let the class do it. Here's how to create NSData using such an array:
#interface DataManager : NSObject
{
#private
int* things;
size_t nThings;
}
- (int*)things;
- (NSData *)thingsAsNSData;
#end
#implementation DataManager
- (int*)things
{
return things;
}
- (NSData *)thingsAsNSData
{
// note: you may need to choose an endianness for serialization
if (0 == nThings) return [NSData data];w
return [NSData dataWithBytes:things length:nThings * sizeof(things[0])];
}
#end
Again - you want the class to create the data because it knows its own structure best.

Weird NSDictionary crash

I have the following code:
NSDictionary *dict = [[NSDictionary alloc]
initWithObjectsAndKeys:myarray1, #"array1", myarray2, #"array2" nil];
NSArray *shorts =[[dict allKeys] sortedArrayUsingSelector:#selector(compare:)];
for (NSString *dir in shorts) {
NSArray *tempArr = [dict objectForKey:dir];
for (NSString *file in tempArr ) {
NSLog(#"%#", file);
}
}
Where myarray1 and myarray2 are NSArrays.
When I execute the code the application crashes with:
-[NSCFString countByEnumeratingWithState:objects:count:]: unrecognized selector sent to instance 0x1d134
This is apparently the tempArr, which is not recognized as an NSArray. I know that [dicFiles objectForKey:dir] returns an id type object, but as a generic type, I cannot get what I'm doing wrong.
You haven't included the code that initializes myarray1 and myarray2, but apparently one or both of them are instances of NSString rather than NSArray. You can check that after retrieving one of the objects from the array as follows:
if (![tempArr isKindOfClass:[NSArray class]])
{
NSLog(#"Unable to process temp array because it's an instance of %#", [tempArr class]);
}
else
{
// for loop code goes here...
}