How to store objects in plist - objective-c

I have some CGPoints that I need to store in a NSDictionary then write to a file. Later, I need to be able to load the file into a NSDictionary and access the CGPoint within.
self.dict is the NSDictionary I want to store points in.
- (void)setPoint:(CGPoint)point forKey:(NSString *)key {
NSValue *value = [NSValue valueWithCGPoint:point];
[self.dict setValue:value forKey:key];
}
I also want the information to be encrypted. So I convert the NSDictionary to NSData to encrypt it.
- (void)encryptDictionaryWithKey:(NSData *)key writeToFile:(NSString *)file {
NSData *encryptedDict = [[NSKeyedArchiver archivedDataWithRootObject:self] encryptWithKey:key];
[encryptedDict writeToFile:file atomically:YES];
}
Then to get the information from the file, decrypt it, and put it in NSDictionary form:
+ (NSDictionary *)dictionaryWithContentsOfEncryptedData:(NSData *)data decryptWithKey:(NSData *)key {
NSData *decryptedData = [data decryptedWithKey:key];
return (NSDictionary *)[NSKeyedUnarchiver unarchiveObjectWithData:decryptedData];
}
I can put some other values (like NSNumber) into the NSDictionary, encrypt it and write it to file, then get it from file and decrypt it... and the value is still in tact. So my code seems to be fine. But it won't work with NSValue.
I use this to get CGPoint from NSValue. At this point, self.plist may have been (but not necessarily) encrypted, written to file, then set to an unencrypted version of the file.
- (CGPoint)pointForKey:(NSString *)key {
NSValue *value = [self.prefs objectForKey:key];
return [value CGPointValue];
}
This code only returns 0,0 (and value == nil) if self.plist has been encrypted, written to file, then loaded from the file and unencrypted.
So the NSValue with CGPoint seems to be set to nil during the process of writing to the file. I have no idea what I did wrong, so any help is appreciated . Thanks!

You can convert the CGPoint into an object that can be stored in a plist. For example, the
CGPointCreateDictionaryRepresentation() function will convert a CGPoint into an NSDictionary (or rather, a CFDictionaryRef which can be cast to an NSDictionary). You can store that in the plist, and then convert it back to a CGPoint using the CGPointMakeWithDictionaryRepresentation() companion function when you are loading the plist.

Related

how to detect pasteboard item type

I am trying to identify between three types of objects:
if it is a URL of a file
If it is a URL of a directory
if it is a simple string
up till now, I have just this code, which does not work!
NSArray * classes = nil;
classes = [[NSArray alloc] initWithObjects:[NSURL class],
[NSAttributedString class],[NSString class], nil];
NSDictionary *options = [NSDictionary dictionary];
NSArray * copiedItems = nil;
copiedItems = [pb readObjectsForClasses:classes options:options];
Now I try to take the first object of the array copiedItems and try to call "types" property and i get a crash!
Check here and here:
You would need to use these pasteboard types, instead of the ones you're using.
NSString *NSStringPboardType;
NSString *NSFilenamesPboardType;
NSString *NSPostScriptPboardType;
NSString *NSTIFFPboardType;
NSString *NSRTFPboardType;
NSString *NSTabularTextPboardType;
NSString *NSFontPboardType;
NSString *NSRulerPboardType;
NSString *NSFileContentsPboardType;
NSString *NSColorPboardType;
NSString *NSRTFDPboardType;
NSString *NSHTMLPboardType;
NSString *NSPICTPboardType;
NSString *NSURLPboardType;
NSString *NSPDFPboardType;
NSString *NSVCardPboardType;
NSString *NSFilesPromisePboardType;
NSString *NSMultipleTextSelectionPboardType;
There's an pasteboard type for URLs. To distinguish between a file and a folder, you would need to instantiate an NSURL object with the pasteboard data, and find out if it is a directory by querying its attributes.
EDIT:
You also need to consider if the pasteboard data is being put there by your own application or other applications. If it's being put by other applications, I'm not sure the pasteboard types with the classes will work.
I use something like this in one of my projects:
supportedTypes = // array with supported types, maybe from the list
NSString *type = [pasteboard availableTypeFromArray:supportedTypes];
NSData *data = [pasteboard dataForType:type];
types is a method on NSPasteboard used to tell you what is available from the pasteboard. So, you shouldn't call it on the items you get back from the pasteboard.
If you're going to request multiple class types, iterate over the response and check the class type of each item, then decide how to interact with it.
Alternatively, decide which class type of data is most useful and make individual class type requests to the pasteboard. If you get a result back, use it and carry on, if not, try the next most useful class type. Look at using canReadObjectForClasses:options: to make this easier.

Write complex object to file objective-c

I find it hard to write/read array of custom objects. In my my app, Contact class has a NSDictionary as property and this dictionary has array as objects for keys.
I serialize/deserialize my objects with NSCoder and NSKeyedArchiever and even tried NSPropertyList serialization. I always get errors when serializing as soon as it starts to serialize NSDictionary. Here is my code and I didn't really find a general answer regarding how to serialize custom objects with complex structure?
//Contact.m
//phoneNumbers is a NSDictionary
#pragma mark Encoding/Decoding
-(void)encodeWithCoder:(NSCoder *)aCoder
{
NSLog(#"Encoding");
[aCoder encodeObject:self.firstName forKey:#"firstName"];
NSLog(#"First name encoded");
[aCoder encodeObject:self.lastName forKey:#"lastName"];
NSLog(#"Last name encoded");
[aCoder encodeInt:self.age forKey:#"age"];
NSLog(#"Age encoded");
NSString *errorStr;
NSData *dataRep = [NSPropertyListSerialization dataFromPropertyList:self.phoneNumbers
format:NSPropertyListXMLFormat_v1_0
errorDescription:&errorStr];
NSLog(#"Data class %#", [dataRep class]);
if(!dataRep)
{
NSLog(#"Error encoding %#", errorStr);
}
[aCoder encodeObject:dataRep forKey:#"phones"];
NSLog(#"Encoding finished");
}
- (id) initWithCoder: (NSCoder *)coder
{
if (self = [super init])
{
[self setFirstName:[coder decodeObjectForKey:#"firstName"]];
[self setLastName:[coder decodeObjectForKey:#"lastName"]];
[self setAge:[coder decodeIntForKey:#"age"]];
NSString *errorStr;
NSData *data=[coder decodeObjectForKey:#"phones"];
NSDictionary *propertyList = [NSPropertyListSerialization propertyListFromData:data mutabilityOption:NSPropertyListImmutable format:NULL errorDescription:&errorStr];
if(!propertyList)
{
NSLog(#"Error %#", errorStr);
}
[self setPhoneNumbers:propertyList];
}
return self;
}
//Serializing/Deserializing an array of Contact objects:
#pragma mark Import/Export
//Export Contacts to file
-(void)exportContactsToFile
{
BOOL done=[NSKeyedArchiver archiveRootObject:self.contacts toFile:[PathUtility getFilePath:#"phonebook"]];
NSLog(#"Export done: %i", done);
}
//Import Contacts from file
-(void)importContactsFromFile
{
self.contacts = [NSKeyedUnarchiver unarchiveObjectWithFile:[PathUtility getFilePath:#"phonebook"]];
}
Is there a generic good way to serialize/deserialize objects in objective-c? thanks
The error I get is:
0objc_msgSend
1 CF_Retain
...
that's stack trace, but I get no other errors(
You shouldn't need to use NSPropertyListSerialization for self.phoneNumbers. NSDictionary adheres to the NSCoding protocol.
So, [aCoder encodeObject:self.phoneNumbers forKey:#"phones"]; should be sufficient.
As long as a class adheres to NSCoding (which nearly all Apple-provided class do), you can just use -encodeObject:forKey:, since that method will call that object's implementation of -encodeWithCoder:
I have a special class in my proprietary library that automatically reads the list of its properties and use the getter and setter to encode and decode the object. Sorry I can't share the code here but I can at least give you steps by steps how my class works:
First, the class must be implement NSCoding and NSCopying protocols.
Inside + (void)initialize, iterate thru the definitions of the properties of the class using class_copyPropertyList(), property_getName() and property_copyAttributeList(). Refer Objective-C Runtime Programming Guide for details on these functions.
For each property, run thru its attribute list and get the attribute with strncmp(attribute.name, "T", 1) == 0 (yup, it's a c-string in there). Use that attribute value to determine the type of the property. For example, "i" means int, "I" means unsigned int, if it starts with a "{" then it's a struct etc. Refer this page on the Type Encoding.
Store the property name-type pairs inside a NSDictionary. At the end of properties iteration, store this dictionary inside a static and global NSMutableDictionary using the class name as the key.
To support auto-encoding, implement - (void)encodeWithCoder:(NSCoder *)aCoder to iterate thru the property name-type pair, calling the property getter method (usually - (returnType)propertyName) and encode it inside the coder using appropriate encodeType: method (e.g. encodeInt:, encodeFloat:, encodeObject:, encodeCGPoint: etc).
To support auto-decoding, implement - (id)initWithCoder:(NSCoder *)aDecoder to iterate thru the property name-type pair, decode it from the decoder using appropriate decodeTypeForKey: method (e.g. decodeIntForKey:, decodeFloatForKey:, decodeObjectForKey:, decodeCGPointForKey: etc). and call the property setter method (usually - (void)setPropertyName:).
Implement an instance method that trigger the encoding (luckily I can share this method here ^__^):
NSMutableData *data = [NSMutableData data];
NSKeyedArchiver *arc = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[arc encodeRootObject:self];
[arc finishEncoding];
[arc release];
return data;
Once you have the NSData you can anything with it such as calling writeToFile:atomically: or even [[NSUserDefaults standardUserDefaults] setObject:[self wrapInNSData] forKey:key].
Also, implement a class method that returns a new instance of the object loaded from the file:
NSKeyedUnarchiver *unarc = [[NSKeyedUnarchiver alloc] initForReadingWithData:[NSData dataWithContentsOfFile:dataFilePath]];
MyCoolFileDataStore *data = [unarc decodeObject];
[unarc release];
return data;
Finally, to make another object class supports this auto-encoding-decoding, the class needs to extend the special class.
Sorry, it's a bit long winded, but for my case, the extra trouble that I took to create this class really save a lot of time along the road. Struggle today, breeze through tomorrow ;)

Saving an NSArray of custom objects

I've created a subclass of UIImage (UIImageExtra) as I want to include extra properties and methods.
I have an array that contains instances of this custom class.However when I save the array, it appears the extra data in the UIImageExtra class is not saved.
UIImageExtra conforms to NSCoding, but neither initWithCoder or encodeWithCoder are called, as NSLog statements I've added aren't printed.
My method to save the array looks like this:
- (void)saveIllustrations {
if (_illustrations == nil) {
NSLog(#"Nil array");
return;
}
[self createDataPath];
//Serialize the data and write to disk
NSString *illustrationsArrayPath = [_docPath stringByAppendingPathComponent:kIllustrationsFile];
NSMutableData *data = [[NSMutableData alloc] init];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[archiver encodeObject:_illustrations forKey:kIllustrationDataKey];
[archiver finishEncoding];
[data writeToFile:illustrationsArrayPath atomically: YES];
}
And the UIImageExtra has the following delegate methods for saving:
#pragma mark - NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder {
NSLog(#"Encoding origin data!");
[super encodeWithCoder:aCoder];
[aCoder encodeObject:originData forKey:kOriginData];
}
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:(NSCoder *) aDecoder]) {
NSLog(#"Decoding origin data");
self.originData = [aDecoder decodeObjectForKey:kOriginData];
}
return self;
}
My code to create the array in the first place looks like this (in case that offers any clues)
for (NSDictionary *illustrationDict in illustrationDicts) {
NSString *illustrationString = [illustrationDict objectForKey:#"Filename"];
NSNumber *xCoord = [illustrationDict objectForKey:#"xCoord"];
NSNumber *yCoord = [illustrationDict objectForKey:#"yCoord"];
UIImageExtra *illustration = (UIImageExtra *)[UIImage imageNamed:illustrationString];
//Scale the illustration to size it for different devices
UIImageExtra *scaledIllustration = [illustration adjustForResolution];
NSValue *originData = [NSValue valueWithCGPoint:CGPointMake([xCoord intValue], [yCoord intValue])];
[scaledIllustration setOriginData:originData];
[self.illustrations addObject:scaledIllustration];
}
Or am I just going about saving this data the wrong way? Many thanks.
Your code to initialize the array is not actually creating instances of your UIImageExtra subclass.
UIImageExtra *illustration = (UIImageExtra *)[UIImage imageNamed:illustrationString];
returns a UIImage. Casting it doesn't do what you were intending.
UIImageExtra *scaledIllustration = [illustration adjustForResolution];
is still just a UIImage.
One straightforward-but-verbose way to approach this would be to make UIImageExtra a wrapper around UIImage. The wrapper would have a class method for initializing from a UIImage:
+ (UIImageExtra)imageExtraWithUIImage:(UIImage *)image;
And then every UIImage method you want to call would have to forward to the wrapped UIImage instance-- also being careful to re-wrap the result of e.g. -adjustForResolution lest you again end up with an unwrapped UIImage instance.
A more Objective-C sophisticated approach would be to add the functionality you want in a Category on UIImage, and then use method swizzling to replace the NSCoding methods with your category implementations. The tricky part of this (apart from the required Objective-C runtime gymnastics) is where to store your "extra" data, since you can't add instance variables in a category. [The standard answer is to have a look-aside dictionary keyed by some suitable representation of the UIImage instance (like an NSValue containing its pointer value), but as you can imagine the bookkeeping can get complicated fast.]
Stepping back for a moment, my advice to a new Cocoa programmer would be: "Think of a simpler way. If what you are trying to do is this complicated, try something else." For example, write a simple ImageValue class that has an -image method and an -extraInfo method (and implements NSCoding, etc.), and store instances of that in your array.
You can't add objects to an NSArray after init. Use NSMutableArray, that might be the issue.

check id isKindOfType CFType

Shortly: how to determine if id is CFType or not at runtime
i'm implementing dynamic core data attributes and in willSave method of ExtendedManagedObject i wanna check if the id value is CFType to store it into plist file.
If I'm trying to save to plist UIImage, that is not toll-free bridged with CF (apple docs), I am getting an error:
2011-11-17 17:16:25.294 [490:707] Error saving extended data: Property list invalid for format (property lists cannot contain objects of type 'CFType')
Can I check it with some method or I have to implement by myself (just isKindOfClass from docs)?
I don't want to implement accessors in NSManagedObject subclass, I dont know exactly how many urls I'll get from entities properties. Question is about dynamic extended attributes at runtime.
- (void)willSave
{
NSDictionary *changes = [self valueForKey:#"extendedChanges"];
if (changes!=nil) {
// merge changes into snapshot
NSMutableDictionary *dict = [[self extendedSnapshot] mutableCopy];
NSEnumerator *e = [changes keyEnumerator];
NSString *key;
while (key=[e nextObject]) {
id value = [changes objectForKey:key];
if (value==[NSNull null])
[dict removeObjectForKey:key];
else if (#ugly and I'm not shure is thread safe **else if ([value isKindOfClass:[NSArray class]] || [value isKindOfClass:[NSDictionary class]] || [value isKindOfClass:[NSDate class]] || [value isKindOfClass:[NSData class]] || [value isKindOfClass:[NSString class]] || [value isKindOfClass:[NSNumber class]])**)
[dict setObject:value forKey:key];
}
// archive as binary plist
NSData *data = nil;
if ([dict count]>0) {
NSString *error=nil;
data = [NSPropertyListSerialization dataFromPropertyList:dict
format:NSPropertyListBinaryFormat_v1_0 errorDescription:&error];
if (error!=nil) {
NSLog(#"Error saving extended data: %#", error);
[error release];
}
}
[dict release];
[self setPrimitiveValue:data forKey:#"extendedData"];
}
[super willSave];
}
That's a wrong way to approach the problem. The blog post you referenced saves the extended attributes as a serialized plist. The plist can contain only the following types of objects, as written in the official doc:
NSArray, NSDictionary, NSDate, NSData, NSString and NSNumber.
Other classes are just not allowed. Don't add objects of any other class to the attributes extended this way.
Also, saving an image file in a CoreData database is not a good idea, generally speaking, read On Blobs in the official doc. Instead, save the UIImage in a file and write the file path in the CoreData database.
If you just have a transient property which is not saved to the database, you don't even have to go through the trouble of creating extended attributes. Just add it as a property of a subclass of NSManagedObject:
#interface MyManagedObject: NSManagedObject {
}
#property(retain, nonatomic) UIImage* thumbnail;
#property(retain, nonatomic) NSDictionary* thumbnailDictionary;
#end
without adding thumbnail in the CoreData model. Then do the corresponding #synthesize in the .m file. The property added this way to a managed object is just not saved.
If you want to keep unknown number of thumbnails, you can put an NSDictionary (or NSArray) containing UIImage*s.
If you really do want to get CFType from an NSObject*, do the following:
NSObject* x= ...;
CFTypeID typeID=CFGetTypeID((CFTypeRef)x);
if(typeID != CFStringGetTypeID() &&
typeID != CFDateGetTypeID() &&
typeID != CFDataGetTypeID() &&
typeID != CFDictionaryGetTypeID() &&
typeID != CFArrayGetTypeID() ) {
... it's a non-plistable type ...
}
The ugly if clause is there because there's no public function which gives you the CFTypeID of an un-bridged Objective-C object.
Read CFType reference. But I don't recommend doing this.
Trying to store an image in UIImage format also results in "property lists cannot contain objects of type CFType error. Best way is to convert UIImage to NSData before writing to pList.

Test NSmutable array from plist before saving

I'm trying to made a cocoa app that read-write to a .plist file.
I can retrieve informations from the .plist, write into, but when a key (only with strings) is empty, the app don't write to the plist.
here a sample:
-
(IBAction)saveBoot:(id)sender {
NSString *errorDesc;
NSString *bootPath = #"/myplist.plist";
NSMutableDictionary *plistBootDict =
[NSMutableDictionary dictionaryWithObjects:
[NSMutableArray arrayWithObjects:
Rescan,
RescanPrompt,
GUI,
InstantMenu,
DefaultPartition,
EHCIacquire,
nil]
forKeys:[NSMutableArray arrayWithObjects:
#"Rescan",
#"Rescan Prompt",
#"GUI",
#"Instant Menu",
#"Default Partition",
#"EHCIacquire",
nil]];
NSData *plistBootData = [NSPropertyListSerialization
dataFromPropertyList:plistBootDict
format:NSPropertyListXMLFormat_v1_0
errorDescription:&errorDesc];
if (bootPath) {
[plistBootData writeToFile:bootPath atomically:NO];
}
else {
NSLog(errorDesc);
[errorDesc release];
}
}
#end
I think i need a loop to check if each key is empty or not (and remove it if empty),
but i've tried different (objectEnumerator, objectForKey:..etc) method whitout success.
If someone can help a beginner like me,
thanks in advance.
Ronan.
The problem is probably that because nil is the terminator for variable argument lists, so if, say, RescanPrompt is nil, the object array will only contain up until that part (so you can't "remove if empty" since it won't exist in the dictionary in the first place). You should probably construct your dictionary piece by piece; something like:
NSMutableDictionary *plistBootDict = [NSMutableDictionary dictionary];
if (Rescan)
[plistBootDisc setObject:Rescan forKey:#"Rescan"];
if (GUI)
[plistBootDisc setObject:GUI forKey:#"GUI"];
// etc
(Also, there's no reason to be using NSMutableArray or NSMutableDictionary if you're never going to be mutating them later.)