Just a quick memory management question if I may ... Is the code below ok, or should I be doing a retain and autorelease, I get the feeling I should. But as per the rules unarchiveObjectWithFile does not contain new, copy or alloc.
-(NSMutableArray *)loadGame {
if([[NSFileManager defaultManager] fileExistsAtPath:[self pathForFile:#"gameData.plist"]]) {
NSMutableArray *loadedGame = [NSKeyedUnarchiver unarchiveObjectWithFile:[self pathForFile:#"gameData.plist"]];
return loadedGame;
} else return nil;
}
or
-(NSMutableArray *)loadGame {
if([[NSFileManager defaultManager] fileExistsAtPath:[self pathForFile:#"gameData.plist"]]) {
NSMutableArray *loadedGame = [[NSKeyedUnarchiver unarchiveObjectWithFile:[self pathForFile:#"gameData.plist"]] retain];
return [loadedGame autorelease];
} else return nil;
}
You are correct in that unarchiveObjectWithFile returns an autoreleased object, since it doesn't contain new, copy or alloc.
Here's a version that is slightly re-written to use common Objective-C formatting idioms:
- (NSMutableArray *)loadGame {
NSString *gameDataPath = [self pathForFile:#"gameData.plist"];
if([[NSFileManager defaultManager] fileExistsAtPath:gameDataPath]) {
return [NSKeyedUnarchiver unarchiveObjectWithFile:gameDataPath];
}
return nil;
}
Related
I am using these two recursive methods to find the paths of files and directories in a certain folder
- (NSMutableArray *)getFilePathsFromDirectory:(NSString *)directory{
NSMutableArray *contents = [[NSMutableArray alloc] init];
NSArray *arr = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:directory error:nil];
for (NSString *file in arr) {
BOOL isDir;
[[NSFileManager defaultManager] fileExistsAtPath:[directory stringByAppendingPathComponent:file] isDirectory:&isDir];
if (!isDir) {
[contents addObject:[directory stringByAppendingPathComponent:file]];
}
else{
[contents addObject:[directory stringByAppendingPathComponent:file]];
[contents addObject:[self getFilePathsFromDirectory:[directory stringByAppendingPathComponent:file]]];
}
}
return contents;
}
- (NSString *)getPathForItemNamed:(NSString *)name array:(NSMutableArray *)arr{
NSString *str;
if (name) {
for (NSString *s in arr) {
if ([s isKindOfClass:[NSString class]]) {
if ([[s lastPathComponent] isEqualToString:name]) {
return s;
}
}
}
for (NSMutableArray *aq in arr) {
if ([aq isKindOfClass:[NSMutableArray class]]) {
str = [self getPathForItemNamed:name array:aq];
return str;
}
}
}
return str;
}
but the problem is, after going through a certain amount of subdirectories (3-5), this stops returning any path and returns (null). I feel like this has to do with the array not being filled with all the directories before it returns for some reason. Heres how I call these
NSMutableArray *paths = [self getContentsOfPaths:[self downloadsDir]];
path = [self getPathForItemNamed:[tableView cellForRowAtIndexPath:indexPath].textLabel.text array:paths];
NSLog(#"%#", path);
There are two problems with your getPathForItemNamed: method:
When it cannot find a file by name, it returns a value of an uninitialized variable str. This is undefined behavior - you need to set str to nil upon initialization. In fact, you do not need str at all (see the fix below).
When it discovers its first subdirectory, it assumes that the file that it is looking for must be inside that subdirectory, even if it is not. Whatever the first-level recursive invocation of getPathForItemNamed: returns, becomes the return result of the top-level invocation. This is bad: if the file that you are looking for is in the subtree of the second subdirectory, you are never going to find it!
Here is how you can fix your method:
- (NSString *)getPathForItemNamed:(NSString *)name array:(NSMutableArray *)arr{
if (!name) return nil;
for (NSString *s in arr) {
if ([s isKindOfClass:[NSString class]]) {
if ([[s lastPathComponent] isEqualToString:name]) {
return s;
}
}
}
for (NSMutableArray *aq in arr) {
if ([aq isKindOfClass:[NSMutableArray class]]) {
str = [self getPathForItemNamed:name array:aq];
// Return something only when you find something
if (str) return str;
}
}
return nil; // You do not need str at all.
}
I want save bool property to my file, and I did it in my opinion is barbaric. I have to check my property and then add string to NSMutableArray. Can I some how check property name, state/value and then save to file? Or maybe I should use XML file for this? But still for efficient use I should get property name and state/value.
Could you give me some advice?
-(void) saveSettings
{
NSString *path = [[NSBundle mainBundle] pathForResource:#"settings" ofType:#""];
if (music)
{
[correctSettingArray removeObjectAtIndex:0];
[correctSettingArray addObject:#"music = 1"];
}
else
{
[correctSettingArray removeObjectAtIndex:0];
[correctSettingArray addObject:#"music = 0"];
}
if (sfx)
{
[correctSettingArray removeObjectAtIndex:1];
[correctSettingArray addObject:#"sfx = 1"];
}
else
{
[correctSettingArray removeObjectAtIndex:0];
[correctSettingArray addObject:#"sfx = 0"];
}
if (vibration)
{
[correctSettingArray removeObjectAtIndex:0];
[correctSettingArray addObject:#"vibration = 1"];
}
else
{
[correctSettingArray removeObjectAtIndex:0];
[correctSettingArray addObject:#"vibration = 0"];
}
[correctSettingArray writeToFile:path atomically:true];
}
Thanks in Advance.
if you want to save simple application settings like this use NSUserDefaults
[[NSUserDefaults standardUserDefaults] setBool:vibrationBool forKey:#"vibrationKey"];
then when you want to read it
BOOL vibrationBool = [[NSUserDefaults standardUserDefaults] boolForKey:#"vibrationKey"];
I think you can save as NSNumber using this...
[NSNumber numberWithBool:BOOLATR]
and retrieve the value doing...
BOOLATR = [[correctSettingArray objectAtIndex:X] boolValue]
In any case, you could prefer to use NSMutableDictionary for variable matching instead an array.
[dictionary setValue:[NSNumber numberWithBool:BOOLATR] forKey:#"BOOLATR"];
&
BOOLATR = [[dictionary valueForKey:#"BOOLATR"] boolValue]
For what you ask — saving user settings — you should use NSUserDefaults as described in answer by wattson12.
If you really need to save boolean properties to file, given you are working with Objective-C objects, easiest way would be to use archive and serialize your data structure by implementing the NSCoding protocol. See Apple's Archives and Serializations Programming Guide.
The NSCoding protocol has two parts: initWithCoder is basically another constructor for your object:
- (id)initWithCoder:(NSCoder *)decoder
{
self = [super init];
if (self) {
if ([decoder containsValueForKey:#"sunNeverSet"])
self.sunNeverSet = [NSNumber numberWithBool:
[decoder decodeBoolForKey:#"sunNeverSet"]];
}
return self;
}
The encodeWithCoder is the serialization:
- (void)encodeWithCoder:(NSCoder *)coder
{
if (sunNeverRise) [coder encodeBool:[sunNeverRise boolValue]
forKey:#"sunNeverRise"];
}
Then you would encode your object graph into platform-independent byte stream (ie. NSData) using the NSKeyedArchiver and write the data to file.
NSMutableData *data = [[NSMutableData alloc] init];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc]
initForWritingWithMutableData:data];
[archiver encodeRootObject:myObjectImplementingNSCoding];
[archiver finishEncoding];
[data writeToFile:path atomically:YES];
To read it back, you'll decode the data using NSKeyedUnarchiver and get back your object graph.
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc]
initForReadingWithData:data];
id myObjectImplementingNSCoding = [[unarchiver decodeObject] retain];
[unarchiver finishDecoding];
Suppose I have a function
- (NSString *)fullNameCopy {
return [[NSString alloc] initWithFormat:#"%# %#", self.firstName, self.LastName];
}
Can somebody tell me how to call this function, how to assign its value to a new object, and how then to release it, avoiding memory leaks, and bad access.
Would it be like
NSSting *abc = [object fullNameCopy];
// Use it and release
[abc release];
or I should alloc abc string too ?
Update:
The point here, Can I return non-autorelease objects from a function and then release them in the calling function. As per Obj-C function naming conventions, a function name containing alloc or copy should return object assuming that calling function has the ownership.
As in above case, my function "fullNameCopy" return a non-autoreleased abject, and I want to release them in the calling function.
You are right. Since the method name contains the word ‘copy’, Cocoa convention dictates that the method returns an object that is owned by the caller. Since the caller owns that object, it is responsible for releasing it. For example:
- (void)someMethod {
NSString *abc = [object fullNameCopy];
// do something with abc
[abc release];
}
Alternatively, you could use -autorelease instead of -release:
- (void)someMethod {
NSString *abc = [[object fullNameCopy] autorelease];
// do something with abc
}
Refer this post
UPDATE:
- (NSString *)fullNameCopy {
NSString *returnString = [NSString stringWithFormat:#"%# %#", self.firstName, self.LastName]; // Autorelease object.
return returnString;
}
-(void) someFunction {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSString *fullName = [self fullNameCopy];
[pool release]
}
Like This:
- (NSString *)fullName {
NSString * retVal = [[NSString alloc] initWithFormat:#"%# %#", self.firstName, self.LastName];
return [retVal autoRelease];
}
Then
NSSting *abc = [object fullName];
return [[[NSString alloc] initWithFormat:#"%# %#", self.firstName, self.LastName]autorelease];
I'm having an "doesNotRecognizeSelector" exception and I suspect that maybe my unarchiver return immutable array intstead of mutable.
Am I right ? how should I do the archiving and archiving properly ? (place of exception is show down)
Thanks!!!
NSMutableArray* arr;
- (void) write
{
NSMutableData *data = [[NSMutableData alloc] init];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
NSMutableArray *copy = [[NSMutableArray arrayWithArray:self.Arr] copy];
[archiver encodeObject:copy forKey:#"Key"];
[archiver finishEncoding];
[data writeToFile:[Util DataPath] atomically:YES];
[archiver release];
[data release];
[copyOfPaidPacks release];
}
-(NSMutableArray*) read
{
NSString* DataPath = [Util FilePath];
NSData *data = [[NSMutableData alloc] initWithContentsOfFile:DataPath];
NSKeyedUnarchiver *unarchiver = nil;
if (data != nil)
{
unarchiver = [[NSKeyedUnarchiver alloc]initForReadingWithData:data];
if([self.Arr count] <= 0)
{
self.Arr = [unarchiver decodeObjectForKey:#"Key"];
}
}
[unarchiver finishDecoding];
[unarchiver release];
[data release];
return self.arr
}
-(void)B
{
[self write];
NSMutableArray* SecondArr = [self read];
[SecondArr sortUsingSelector:#selector(CompareDate:)]; - > `****THIS IS WHERE I GET THE EXCEPTION`
}
Edit
Adding the compare method:
- (NSComparisonResult)CompareDate:(T*)p
{
NSComparisonResult result = [self.changeDate compare:p.changeDate];
if(result == NSOrderedDescending)
result = NSOrderedAscending;
else if(result == NSOrderedAscending)
result = NSOrderedDescending;
if(result == NSOrderedSame)
{
result = [self CompareName:p];
}
return result;
}
NSKeyedUnarchiver does indeed return an immutable NSArray. If you really want to get an NSMutableArray, you'd need to call -mutableCopy on the return value from decodeObjectForKey:.
This code snippet makes me wonder if you really even need a mutable array though. It looks like you're just sorting the array you get from -read. Why not just call sortedArrayUsingSelector: on the immutable NSArray instead?
You already pass an immutable array to the archiver, so why would you expect the unarchiver to return a mutable one then?
If you want a copy to be mutable, then you have to use
NSMutableArray *copy = [[NSMutableArray arrayWithArray:self.Arr] mutableCopy];
as
NSMutableArray *copy = [[NSMutableArray arrayWithArray:self.Arr] copy];
will create an immutable copy (at least as far, as you should be concerned. In reality the framework will often use a mutable internal representation for immutable instances, but this is an implementation detail and you should not count on it, as these representations have changed in the past and the Cocoa docs explicitly tell you, to not check for the internal representation).
EDIT:
just tested with NSKeyedArchiver myself on iOS 5.0 simulator:
// this returns a mutable instance at runtime!
NSMutableArray* test0 = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:[NSMutableArray array]]];
// this returns an immutable instance at runtime!
NSArray* test1 = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:[NSArray array]]];
So your problem is really exclusively caused by using copy instead of mutableCopy, while archiving and unarchiving keeps the correct mutability-attributes of the instances!
I had an issue where archiving a 3-dimensional mutable array (ie, a mutable array of mutable arrays of mutable arrays) would unarchive as a mutable array of immutable arrays of immutable arrays.
The key to fixing this was to subclass NSKeyedUnarchiver and overwrite
- (Class)classForClassName:(NSString *)codedName
{
Class theClass = [super classForClassName: codeName];
if ([codeName isEqualToString: NSStringFromClass(NSArray.class))
{
theClass = NSMutableArray.class;
}
//... more here like NSDictionary
return theClass;
}
I'm saving some data using a series of NSDictionaries, stored in an NSMutableArray and archived using NSKeyedArchiver.
I'm basically trying to save the states of several instances the class 'Brick', so I've implemented a getBlueprint method like this (slimmed down version)
-(id)getBlueprint
{
// NOTE: brickColor is a string
NSDictionary *blueprint = [NSDictionary dictionaryWithObjectsAndKeys:
brickColor, #"color",
[NSNumber numberWithInt:rotation], #"rotation",
nil];
return blueprint;
}
And so I have another method that creates a new Brick instance when provided with a blueprint.
-(id)initWithBlueprint:(NSDictionary *)blueprint spriteSheet:(NSString *)ssheet
{
if((self == [super init])){
brickColor = [blueprint objectForKey:#"color"];
[self setColorOffset:brickColor];
while(rotation != [[blueprint objectForKey:#"rotation"] intValue]){
[self setRotation:90];
}
}
return self;
}
Which works when I pass it a 'fresh' blueprint, but not when I read a blueprint from a saved file... sort of. For example, the rotation will work, but changing the color wont. So while I can read the value of brickColor using
NSLog(#"brick color %#", [blueprint objectForKey:#"color"]);
if I try something like
if(brickColor == #"purple"){
colorOffset = CGPointMake(72,36);
NSLog(#"Changed offset for -- %# -- to %#", color, NSStringFromCGPoint(colorOffset));
}
And I know that color is purple, the condition doesn't return true. I thought it might be that somehow NSKeyedUnarchiver changed a string into something else, but the following test returns true.
if([color isKindOfClass:[NSString class]]){
NSLog(#"%# IS A STRING", color);
}else{
NSLog(#"!!!!! COLOR IS A NOT STRING !!!!!");
}
As I said, this isn't a problem if I try to use a freshly created NSDictionary as a blueprint, only when a blueprint is archived and then read back in.
So, as usual, I'm wondering if anyone has any ideas why this might be happening.
incase it's relevant, here's how the data is being stored and recieved.
// Saving
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
-(void)buildLevelData{
levelData = [[NSMutableArray alloc] initWithCapacity:100];
for(brickSprite *brick in spriteHolder.children){
[levelData addObject:[brick getBlueprint]];
}
}
-(void)saveLevel
{
[self buildLevelData];
NSData *rawDat = [NSKeyedArchiver archivedDataWithRootObject:levelData];
if([self writeApplicationData:rawDat toFile:saveFileName]){
NSLog(#"Data Saved");
}else{
NSLog(#"ERROR SAVING LEVEL DATA!");
}
[[Director sharedDirector] replaceScene:[MainMenu scene]];
}
- (BOOL)writeApplicationData:(NSData *)data toFile:(NSString *)fileName {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
if (!documentsDirectory) {
NSLog(#"Documents directory not found!");
return NO;
}
NSString *appFile = [saveDir stringByAppendingPathComponent:fileName];
return ([data writeToFile:appFile atomically:YES]);
}
// Loading
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
- (void) loadRandomMapFrom:(NSString *)dir
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *docsDir = [paths objectAtIndex:0];
if(!docsDir){
NSLog(#"Cound Not Find Documents Directory When trying To Load Random Map");
return;
}
dir = [docsDir stringByAppendingPathComponent:[NSString stringWithFormat:#"/%#", dir]];
// we'll also set the file name here.
NSArray *existingFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:dir error:nil];
// get last file for this test
NSString *filePath = [dir stringByAppendingPathComponent:[existingFiles objectAtIndex:([existingFiles count] - 1)]];
NSMutableArray *levelData = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
[self buildMapWithData:levelData];
}
-(void)buildMapWithData:(NSMutableArray *)lData
{
for(NSDictionary *blueprint in lData){
brickSprite *brick = [[brickSprite alloc] initWithBlueprint:blueprint spriteSheet:#"blocks.png"];
[spriteHolder addChild:brick];
}
}
Sorry about the mess of a question. There's a lot going on that I'm struggling to fully understand myself so it's hard to break it down to the bare minimum.
You should always compare strings with [firstString isEqualToString:secondString], because firstString == secondString only checks for pointer equality, e.g. if both strings are stored at the same location (which they'll never be when comparing dynamically created objects and string constants).