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"];
Related
The requirement is that I want to store NSArray of my custom objects in NSUserDefaults.
Following is the code from my example
- (void)viewDidLoad
{
[super viewDidLoad];
sampleArray=[[NSMutableArray alloc]init];
MyClass *obj1=[[MyClass alloc]init];
obj1.name=#"Reetu";
obj1.countOpen=1;
NSArray *subArray = [[NSArray alloc]initWithObjects:#"likes131",#"likes132", #"likes133", nil];
obj1.hobbies = [[NSArray alloc]initWithObjects:#"like11", #"like12", subArray, nil];
[sampleArray addObject:obj1];
MyClass *obj2=[[MyClass alloc]init];
obj2.name=#"Pinku";
obj2.countOpen=2;
NSArray *subArray2 = [[NSArray alloc]initWithObjects:obj1 ,#"likes231",#"likes232", #"likes233", obj1 ,nil];
obj2.hobbies = [[NSArray alloc]initWithObjects:#"like21", #"like22", subArray2 ,nil];
[sampleArray addObject:obj2];
MyClass *obj3=[[MyClass alloc]init];
obj3.name=#"Mike";
obj3.countOpen=6;
obj3.hobbies = [[NSArray alloc]initWithObjects:obj1 , obj2 ,#"likes000", nil];
[sampleArray addObject:obj3];
//First lets encode it
NSUserDefaults *userDefault=[NSUserDefaults standardUserDefaults];
NSData *myEncodedObject = [NSKeyedArchiver archivedDataWithRootObject:sampleArray];
[userDefault setObject:myEncodedObject forKey:[NSString stringWithFormat:#"sample"]];
[userDefault synchronize];
//Lets decode it now
NSData *myDecodedObject = [userDefault objectForKey: [NSString stringWithFormat:#"sample"]];
NSArray *decodedArray =[NSKeyedUnarchiver unarchiveObjectWithData: myDecodedObject];
//Print the array received from User's Default
for (MyClass *item in decodedArray) {
NSLog(#"name=%#",item.name);
NSLog(#"LIKES TO %#",item.hobbies);
}
}
This is my custom class confirming to the NSCoding protocol
- (void)encodeWithCoder:(NSCoder *)encoder
{
//Encode properties, other class variables, etc
[encoder encodeObject:self.name forKey:#"name"];
[encoder encodeObject:[NSNumber numberWithInt:self.countOpen] forKey:#"destinationCode"];
[encoder encodeObject:self.hobbies forKey:#"likesTo"];
}
- (id)initWithCoder:(NSCoder *)decoder
{
self = [super init];
if( self != nil )
{
self.name = [decoder decodeObjectForKey:#"name"];
self.countOpen = [[decoder decodeObjectForKey:#"countOpen"] intValue];
self.hobbies = [decoder decodeObjectForKey:#"likesTo"];
}
return self;
}
here is the output:-
2013-10-22 17:01:47.118 Sample[1056:c07] name=Reetu
2013-10-22 17:01:47.120 Sample[1056:c07] LIKES TO (
like11,
like12,
(
likes131,
likes132,
likes133
)
)
2013-10-22 17:01:47.121 Sample[1056:c07] name=Pinku
2013-10-22 17:01:47.123 Sample[1056:c07] LIKES TO (
like21,
like22,
(
"<MyClass: 0x6e32910>",
likes231,
likes232,
likes233,
"<MyClass: 0x6e32910>"
)
)
2013-10-22 17:01:47.125 Sample[1056:c07] name=Mike
2013-10-22 17:01:47.127 Sample[1056:c07] LIKES TO (
"<MyClass: 0x6e32910>",
"<MyClass: 0x6e1f610>",
likes000
)
The problem is <MyClass: 0X632910>. I was expecting it to be contents of obj1 itself.
The problem isn't with NSUserDefaults. It's with how you are printing the information out:
You should override -(NSString*) description, probably to read something like this:
-(NSString*) description
{
return [NSString stringWithFormat:#"<%# - name:%# open:%d>", self.class self.name, self.countOpen];
}
You are not logging the way like you are adding the elements to the array. You are adding a MyClass object to the second array but you are expecting its hobbies NSArray to be printed.
Change your logging function like this. It will print corretly until the second object. For the third object you have to write another for loop in the function. Because your object hierarchy is like that.
for (MyClass *item in decodedArray) {
NSLog(#"name=%#",item.name);
if ([item.technologies isKindOfClass:[MyClass class]]) {
for (MyClass *item in arr) {
NSLog(#"name=%#",item.name);
NSLog(#"LIKES TO %#",item.hobbies);
}
}
else{
NSLog(#"LIKES TO %#",item.hobbies);
}
}
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
I have a custom object class with the following .m:
#implementation FolderObject
#synthesize folderTitle, folderContents; //title is NSString, contents is array
- (id)init
{
self = [super init];
if (self) {
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:foldersContents forKey:#"foldersContents"];
[coder encodeObject:folderTitle forKey:#"folderTitle"];
}
- (id)initWithCoder:(NSCoder *)coder {
self = [[FolderObject alloc] init];
if (self != nil)
{
foldersContents = [coder decodeObjectForKey:#"foldersContents"];
folderTitle = [coder decodeObjectForKey:#"folderTitle"];
}
return self;
}
#end
Here is how I use the folder (in some other class):
FolderObject *newFolder=[FolderObject alloc];
newFolder.folderTitle=[textView text];
newFolder.folderContents=[[NSMutableArray alloc]init];
[folders addObject:newFolder];
Here is how I save and retrieve the custom object from NSUserDefaults:
-(void)viewDidDisappear:(BOOL)animated
{
[[NSUserDefaults standardUserDefaults] setObject:[NSKeyedArchiver archivedDataWithRootObject:folders] forKey:#"folders"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
-(void)viewWillAppear:(BOOL)animated
{
NSUserDefaults *currentDefaults = [NSUserDefaults standardUserDefaults];
NSData *dataRepresentingSavedArrayFolders = [currentDefaults objectForKey:#"folders"];
if (dataRepresentingSavedArrayFolders != nil)
{
NSArray *oldSavedArray = [NSKeyedUnarchiver unarchiveObjectWithData:dataRepresentingSavedArrayFolders];
if (oldSavedArray != nil) {
folders = [[NSMutableArray alloc] initWithArray:oldSavedArray];
}
else {
folders = [[NSMutableArray alloc] init];
}
}
else folders=[[NSMutableArray alloc] init];
And finally here is where the issue is:
FolderObject *newFolder=[folders objectAtIndex:indexPath.row];
[folderTitleLabel setText:newFolder.folderTitle];
On the second line, I get an error during runtime, but only after I exit the app and come back. When I add objects to the folders array and call the above, no problems. But if I exit the app and come back, then problems:
-[__NSArrayM isEqualToString:]: unrecognized selector sent to instance 0x4b9e400
2011-09-02 08:09:24.290 MyApp[43504:b303] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSArrayM isEqualToString:]: unrecognized selector sent to instance 0x4b9e400'
Inside of initWithCoder: for your FolderObject you are not properly retaining the values which will result in your crash. Make sure you use the property like this.
- (id)initWithCoder:(NSCoder *)coder {
self = [super init];
if (self != nil)
{
//dot notation (self.) will properly retain or copy your string
//as long as it is declared as retain or copy (I recommend copy)
self.foldersContents = [coder decodeObjectForKey:#"foldersContents"];
self.folderTitle = [coder decodeObjectForKey:#"folderTitle"];
}
return self;
}
The problem seems to be in your initWithCoder: method:
foldersContents = [coder decodeObjectForKey:#"foldersContents"];
folderTitle = [coder decodeObjectForKey:#"folderTitle"];
The values returned by decodeObjectForKey: are not retained for you, and as you are assigning them to ivars directly instead of via the (presumably) retain-declared properties, they are not being retained there either. So they get auto-released the next time the autorelease pool is drained. By the time you get around to trying to use it, it just so happens that the memory location formerly used for the title is now occupied by an __NSArrayM object; were things to work out slightly differently, you'd get a more straightforward EXC_BAD_ACCESS crash.
#Joe is right about needing to retain the objects but why are you using
self = [[FolderObject alloc] init];
instead of
self = [super init];
Your code will leak an chunk of memory each time it's called?
I don't know if it's causing your crash (I suspect not) but it's certainly interesting.
I have a problem. There is class to store game progress:
struct GameData {
PackData & packDataById(LEVEL_PACK packId);
int gameVersion;
AudioData audio;
PackData sunrise;
PackData monochrome;
PackData nature;
};
//singleton
#interface GameDataObject : NSObject <NSCoding>
{
GameData data_;
}
+(GameDataObject*) sharedObject;
-(id) initForFirstLaunch;
-(GameData*) data;
-(void) save;
#end
and implementation:
#implementation GameDataObject
static GameDataObject *_sharedDataObject = nil;
+ (GameDataObject*) sharedObject
{
if (!_sharedDataObject) {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *encodedObject = [defaults objectForKey:Key];
if (!encodedObject) {
_sharedDataObject = [[GameDataObject alloc] initForFirstLaunch];
}
else {
_sharedDataObject = (GameDataObject*)[NSKeyedUnarchiver unarchiveObjectWithData: encodedObject];
}
}
return _sharedDataObject;
}
-(GameData*) data
{
return &data_;
}
-(id) initForFirstLaunch
{
self = [super init];
if (self) {
data_.audio.reset();
data_.sunrise.reset();
data_.monochrome.reset();
data_.nature.reset();
data_.gameVersion = 1;
data_.sunrise.levelData[0].state = LEVEL_OPENED;
}
return self;
}
-(void) save
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:self] forKey:Key];
[defaults synchronize];
}
-(void) encodeWithCoder:(NSCoder *)encoder
{
[encoder encodeInt:data_.gameVersion forKey:#"game-version"];
[encoder encodeBytes:(uint8_t*)&data_.audio length:sizeof(AudioData) forKey:#"audio-data"];
[encoder encodeBytes:(uint8_t*)&data_.sunrise length:sizeof(PackData) forKey:#"sunrise-pack"];
[encoder encodeBytes:(uint8_t*)&data_.monochrome length:sizeof(PackData) forKey:#"monochrome-pack"];
[encoder encodeBytes:(uint8_t*)&data_.nature length:sizeof(PackData) forKey:#"nature-pack"];
}
-(id) initWithCoder:(NSCoder *)decoder
{
self = [super init];
if (self) {
data_.gameVersion = [decoder decodeIntForKey:#"game-version"];
NSUInteger length = 0;
{
const uint8_t *buffer = [decoder decodeBytesForKey:#"audio-data" returnedLength:&length];
assert(length);
memcpy(&data_.audio, buffer, length);
}
{
const uint8_t *buffer = [decoder decodeBytesForKey:#"sunrise-pack" returnedLength:&length];
assert(length);
memcpy(&data_.sunrise, buffer, length);
}
{
const uint8_t *buffer = [decoder decodeBytesForKey:#"monochrome-pack" returnedLength:&length];
assert(length);
memcpy(&data_.monochrome, buffer, length);
}
{
const uint8_t *buffer = [decoder decodeBytesForKey:#"nature-pack" returnedLength:&length];
assert(length);
memcpy(&data_.nature, buffer, length);
}
}
return self;
}
#end
It loads and saves itself correctly when save is called directly after initialization and nothing more is done.
But when I try a simple thing. I write in appDidFinishLaunching
GameDataObject *obj = [GameDataObject sharedObject];
Then everything is done - just one simple menu is loaded, and I minimize the application so
-(void) applicationDidEnterBackground:(UIApplication*)application
{
[[CCDirector sharedDirector] stopAnimation];
[[GameDataObject sharedObject] save];
}
is executed. And in this method obj is totally corrupted (before saving), sometimes it's even seen with debugger as another class object.
What am I doing wrong?
EDIT
Just launching the app and minimizing it causes the same problem.
As I already mentioned in the comment, you have a memory bug when unarchiving the data using NSKeyedUnarchiver.
The method +[NSKeyedUnarchiver unarchiveObjectWithData:] returns an autoreleased object (you can tell from the naming convention: it doesn't contain either new, alloc or copy) so you'd have to take ownership of the object by sending it a retain message. Now the object won't be released by the autorelease pool at the end of the runloop.
While you have an answer, your overall app architecture could use a bit of refinement.
Notably, it is generally quite fragile to have some massive amount of persistent logic associated with an arbitrary singleton's instantiation. It introduces all kinds of weird ordering dependencies or other mechanisms via which a seemingly minor change can cause your code to break.
A far less fragile pattern is to associate reconstruction of state with known points in an application's lifespan. I.e. if the state is required for the app to work, load the state in applcationDidFinishLaunching:. If the state is only required by a subsystem, load it when the subsystem is loaded.
Doing so reduces complexity and, by implication, reduces the maintenance costs of your code. Any indeterminism you can eliminate is a future bug removed.
When reading an archived shared object, you must retain it when assigning to your singleton variable. NSCoder methods for unarchiving always return autoreleased objects.
which is the cleanest way to use something like a global variable? Normally, using a global variable is forbidden, but I don't know a better solution for accessing NSUserDefaults from different classes.
I read a bit and come up with this. I define a Contants.h and a Constants.m file and include them everywhere I need to.
//Constants.h
#import <Foundation/Foundation.h>
#interface Constants : NSObject {
extern NSUserDefaults *settings;
}
#end
.
//Constants.m
#implementation Constants
NSString *filePath = [[NSBundle mainBundle] pathForResource:#"Settings" ofType:#"plist"];
NSDictionary *settingsDict = [NSDictionary dictionaryWithContentsOfFile:filePath];
[[NSUserDefaults standardUserDefaults] registerDefaults:settingsDict];
NSUserDefaults *settings = [NSUserDefaults standardUserDefaults];
#end
The problem here is that I want to initialize a value to my constant. I have no method in Constants.m. So my helper variables would also be globals?
One thing to mention: I think the global variable also has to be released?
Thanks for your help!
Edit:
#hotpaw2:
AppBundleSingleton.h:
#import <Foundation/Foundation.h>
#interface AppBundleSingleton : NSObject {
}
+ (AppBundleSingleton *)sharedAppBundleSingleton;
#end
AppBundleSingleton.m:
#import "AppBundleSingleton.h"
static AppBundleSingleton *sharedAppBundleSingleton = nil;
#implementation AppBundleSingleton
#pragma mark -
#pragma mark Singleton methods
+ (AppBundleSingleton *)sharedAppBundleSingleton {
#synchronized(self) {
if (sharedAppBundleSingleton == nil) {
sharedAppBundleSingleton = [[self alloc] init];
}
}
return sharedAppBundleSingleton;
}
+ (id)allocWithZone:(NSZone *)zone {
#synchronized(self) {
if (sharedAppBundleSingleton == nil) {
sharedAppBundleSingleton = [super allocWithZone:zone];
return sharedAppBundleSingleton; // assignment and return on first allocation
}
}
return nil; // on subsequent allocation attempts return nil
}
- (id)copyWithZone:(NSZone *)zone {
return self;
}
- (id)retain {
return self;
}
- (NSUInteger)retainCount {
return NSUIntegerMax; //denotes an object that cannot be released
}
- (void)release {
//do nothing
}
- (id)autorelease {
return self;
}
-(id)init {
self = [super init];
sharedAppBundleSingleton = self;
// Initialization code here
NSString *filePath = [[NSBundle mainBundle] pathForResource:#"Settings" ofType:#"plist"];
NSDictionary *settingsDict = [NSDictionary dictionaryWithContentsOfFile:filePath];
[[NSUserDefaults standardUserDefaults] registerDefaults:settingsDict];
return self;
}
#end
In my AppDelegate.m I have the following:
// ...
#include "AppBundleSingleton.h"
#implementation MyAppDelegate
// ...
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
// Add the navigation controller's view to the window and display.
[window addSubview:navigationController.view];
[window makeKeyAndVisible];
[AppBundleSingleton sharedAppBundleSingleton];
return YES;
}
// ...
#end
In my ViewController I query the values:
NSString *myString = [[NSUserDefaults standardUserDefaults] stringForKey:#"myKeyforString"];
Would that be a solution?
You don't need to use global variables - you can access the userdefaults from any class or object within your project like this:
BOOL foo = [[NSUserDefaults standardUserDefaults] boolForKey:#"bar"];
As long as you synchronize the userdefaults after every change you make, the data fetched in this way is consistent.
Global variables are not only NOT forbidden, but a required part of the ANSI C specification, which is a subset of Obj C. Both gcc and llvm fully support globals. They are often the smallest and fastest way to pass unprotected values around.
That said, explicit use of global variables are most probably not the solution to your problem. You have a problem quite well suited to the MVC paradigm. Place all your NSDefault code into a singleton model class (an M of the MVC), which can self-initialize on first access (the implementation may use a hidden global), and attach that object to the appDelegate where it can easily be obtained from anywhere. Then encapsulate all you default value read/writes as properties of that object.