Issue with save file to Documents NSMutableArray with NSObjects - objective-c

I'm a newbie iOS developer.
I wrote a small application that save an NSMutableArray array with my objects that derived from NSObject.
Application do the save but the file isn't created in document directory and application can't read.
this issue is both on the simulator and my iPhone 3gs 4.2.1
My NSMutableArray definition inside the appDelegate class:
#property (nonatomic,retain, readwrite) NSMutableArray *places;
My NSObject class:
#import <Foundation/Foundation.h>
#interface Place : NSObject {
NSString *name;
NSString *location;
}
-(id) init:(NSString *)name: (NSString *)location;
#property (retain,nonatomic,readwrite) NSString *name;
#property (retain,nonatomic,readwrite) NSString *location;
#end
My StorageService library class:
#import "StorageService.h"
#implementation StorageService
-(id) init {
self = [super init];
if (self != nil) {
}
return self;
}
-(void) saveArrayToFile:(NSString*) filename : (NSMutableArray *)arrayToSave{
// get full path
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *fullPath = [paths objectAtIndex:0];
fullPath = [fullPath stringByAppendingPathComponent:filename];
NSLog(#"Save in %#",fullPath);
[arrayToSave writeToFile:fullPath atomically:YES];
}
-(NSMutableArray*) readArrayFromFile:(NSString *)filename {
// get full path
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *fullPath = [paths objectAtIndex:0];
fullPath = [fullPath stringByAppendingPathComponent:filename];
if ([[NSFileManager defaultManager] fileExistsAtPath:fullPath]) {
NSMutableArray *data = [[NSMutableArray alloc] initWithContentsOfFile:fullPath];
if (data == nil) {
data = [[NSMutableArray alloc] init];
}
NSLog(#"Read from %#",fullPath);
return data;
} else {
NSMutableArray *data = [[NSMutableArray alloc] initWithContentsOfFile:fullPath];
return data;
}
}
-(void) dealloc {
[super dealloc];
}
#end
and My functions in the appDelegate:
-(void) saveApplicationData {
[self.storageService saveArrayToFile : PLACES_FILE : self.places];
}
-(void) loadApplicationData {
self.places = [self.storageService readArrayFromFile:PLACES_FILE];
}
Here is my class that holds constant to filename:
#import <Foundation/Foundation.h>
extern NSString * const PLACES_FILE = #"Places.dat";
#interface ApplicationConstants : NSObject {
}
#end
So what is wrong?
Thank you guys.

What you want is to let Place conform to the NSCoding protocol, to allow for serialization to and from files (and in memory data if wanted)
Extend Place as (I have also changed the name of the init method as your name was against every naming practice iOS has):
#interface Place : NSObject <NSCoding> {
NSString *name;
NSString *location;
}
-(id)initWithName:(NSString *)name location:(NSString *)location;
#property (retain,nonatomic,readwrite) NSString *name;
#property (retain,nonatomic,readwrite) NSString *location;
#end
Your implementation is quite simple but you also need to implement two methods defined by the NSCoding protocol:
#implementation Place
#synthesize name, location;
-(id)initWithName:(NSString *)aName location:(NSString *)aLocation {
self = [super init];
if (self) {
self.name = aName;
self.location = aLocation;
}
return self;
}
-(id)initWithWithCoder:(NSCoder)decoder {
self = [super initWithCoder:decoder];
if (self) {
self.name = [decoder decodeObjectForKey:#"name"];
self.location = [decoder decodeObjectForKey:#"location";
}
return self;
}
-(void)encodeWithCoder:(NSCoder*)encoder {
[encoder encodeObject:self.name forKey:#"name"];
[encoder encodeObject:self.location forKey:#"location"];
[super encodeWithCoder:encoder];
}
#end
With this in place, saving the places array to disk is as easy as:
[NSKeyedArchiver archiveRootObject:places toFile:path];
And decoding just as easy:
places = [[KSKeyUnarchiver unarchiveObjectWithFile:path] retain];

To use writeToFile objects in array need to be plist capable type (NSDate, NSDate, NSString, NSArray, NSDictionary)
Implement NSCoding on the objects in array and use NSKeyedArchiver to serialize/deserialize.
write:
[NSKeyedArchiver archiveRootObject:myArray toFile:self.places];
read:
[NSKeyedUnarchiver unarchiveObjectWithFile:path];
More info can be found here:
Persisting Custom Objects

Related

Is there any way to emulate user's input in XCTest function?

I have method which expects user's input
#implementation TeamFormation
- (void)run {
NSFileHandle *kbd = [NSFileHandle fileHandleWithStandardInput];
NSData *inputData = [kbd availableData];
NSString *option = [[[NSString alloc] initWithData:inputData
encoding:NSUTF8StringEncoding] substringToIndex:1];
NSLog(#"%#",option);
}
#end
Then I would like to cover this method by a test case
#interface TeamFormationTests : XCTestCase
#end
#implementation TeamFormationTests
- (void)testTeamFormation {
TeamFormation *teamFormation = [TeamFormation new];
[teamFormation run];
// emulate user's input here
}
#end
So, how to emulate user's input in test case function?
You have many options how to achieve this. Two obvious below.
Change run to accept an argument
- (void)run to - (void)runWithFileHandle:(NSFileHandle *)handle
your app code can pass stdin filehandle
your test code can pass handle to a file with desired input
Mock it with protocol
Create DataProvider protocol:
#protocol DataProvider
#property(readonly, copy) NSData *availableData;
#end
Make NSFileHandle to conform to this protocol:
#interface NSFileHandle (AvailableDataProvider) <DataProvider>
#end
Store an object implementing this protocol on TeamFormation:
#interface TeamFormation : NSObject
#property (nonatomic, nonnull, strong) id<DataProvider> dataProvider;
- (NSString *)run;
#end
By default, use stdin file handle:
#implementation TeamFormation
- (instancetype)init {
if ((self = [super init]) == nil) {
return nil;
}
_dataProvider = [NSFileHandle fileHandleWithStandardInput];
return self;
}
- (NSString *)run {
NSData *inputData = [self.dataProvider availableData];
return [[[NSString alloc] initWithData:inputData encoding:NSUTF8StringEncoding] substringToIndex:1];
}
#end
Create TestDataProvider in your test:
#interface TestDataProvider: NSObject<DataProvider>
#property (nonatomic, strong, nonnull) NSData *dataToProvide;
#end
#implementation TestDataProvider
- (instancetype)init {
if ((self = [super init]) == nil) {
return nil;
}
_dataToProvide = [NSData new];
return self;
}
- (NSData *)availableData {
return _dataToProvide;
}
#end
And use it in TestFormationTests:
#implementation TeamFormationTests
- (void)testFormationRun {
TestDataProvider *dataProvider = [TestDataProvider new];
TeamFormation *formation = [TeamFormation new];
formation.dataProvider = dataProvider;
XCTAssertThrows([formation run]);
dataProvider.dataToProvide = [#"foo" dataUsingEncoding:NSUTF8StringEncoding];
XCTAssertEqualObjects([formation run], #"f");
dataProvider.dataToProvide = [#"bar" dataUsingEncoding:NSUTF8StringEncoding];
XCTAssertEqualObjects([formation run], #"b");
}
#end

Issue with writing XMLDocument

I have a non doc-based app with table view with two columns, whose Value bindings are bound to two properties of the arrangedObjects of an array controller. I have a NSMutableArray in my AppDelegate that hold the data, as well as the corresponding #property and #synthesize. I implement key-value-coding accessor methods and use those when inserting data to the array in order to generate KVO notifications. I fill up my array with some NSDictionary objects from xml file. I've got all that working (modify, remove and add items), but what I'd like to do is write it back to xml file.
xmlData writeToFile:fileName atomically:YES
is writing to disk, but don't reflect changes when editing table view. There must be some tiny little thing I am forgetting. What is it?
Any suggestions would be very nice!
The code:
// AppDelegate.h
#import <Cocoa/Cocoa.h>
#interface AppDelegate : NSObject <NSApplicationDelegate>{
NSXMLDocument *xmlDoc;
NSXMLElement *root;
NSMutableArray *children;
}
#property (assign) IBOutlet NSWindow *window;
#property (nonatomic, copy)NSArray *children;
- (void)setChildren:(NSArray *)newChildren;
- (NSUInteger)countOfChildren;
- (id)objectInChildrenAtIndex:(NSUInteger)index;
- (void)insertObject:(id)object inChildrenAtIndex:(NSUInteger)index;
- (void)removeObjectFromChildrenAtIndex:(NSUInteger)index;
- (IBAction)addObject:(id)sender;
- (IBAction)saveXMLToFile:(id)sender;
- (void) populateTable;
#end
// AppDelegate.m
#import "AppDelegate.h"
#implementation AppDelegate
-(void) addObjectToChildren:(id)object{
[self insertObject:object inChildrenAtIndex:[self countOfChildren]];
}
#synthesize children;
- (id)init{
self = [super init];
if (self) {
children = [[NSMutableArray alloc] init];
}
return self;
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification{
[self populateTable];
}
#pragma mark - Indexed Accessors
- (void)setChildren:(NSArray *)newChildren{
if (children!=newChildren) {
children=[newChildren mutableCopy];
}
}
-(NSUInteger)countOfChildren{
return [self.children count];
}
-(id)objectInChildrenAtIndex:(NSUInteger)index{
return [self.children objectAtIndex:index];
}
- (void)insertObject:(id)object inChildrenAtIndex:(NSUInteger)index{
[children insertObject:object atIndex:index];
}
-(void)removeObjectFromChildrenAtIndex:(NSUInteger)index{
[children removeObjectAtIndex:index];
}
#pragma mark -
-(void)populateTable{
NSError *error=nil;
NSString *pathname = [#"~/myFile.xml" stringByExpandingTildeInPath];
NSURL *url = [NSURL fileURLWithPath:pathname];
xmlDoc = [[NSXMLDocument alloc] initWithContentsOfURL:url options:NSXMLDocumentTidyXML error:&error];
root = [xmlDoc rootElement];
NSArray *array=[root nodesForXPath:#"number" error:&error];
NSMutableArray *mArray=[[NSMutableArray alloc]init];
for (NSXMLElement *element in array) {
NSMutableDictionary *dict=[NSMutableDictionary dictionaryWithObjectsAndKeys:[element stringValue],#"id",#"number", #"name", nil];
[mArray addObject:dict];
}
[self setChildren:[NSArray arrayWithArray:mArray]];
}
#pragma mark - Actions
- (IBAction)addObject:(id)sender {
NSMutableDictionary *dict=[NSMutableDictionary dictionaryWithObjectsAndKeys:#"0",#"id",#"number", #"name", nil];
NSXMLElement *element=[[NSXMLElement alloc]initWithName:[dict objectForKey:#"name"] stringValue:[dict objectForKey:#"id"]];
[root insertChild:element atIndex:[self countOfChildren]];
[self addObjectToChildren:dict];
}
- (IBAction)saveXMLToFile:(id)sender {
NSString *fileName = [#"~/myFile.xml" stringByExpandingTildeInPath];
NSData *xmlData = [xmlDoc XMLDataWithOptions:NSXMLNodePrettyPrint];
if (![xmlData writeToFile:fileName atomically:YES]) {
NSBeep();
NSLog(#"Could not write document out...");
}
}
#end
The xml file:
<root>
<number>0</number>
<number>1</number>
<number>2</number>
<number>3</number>
<number>4</number>
<number>5</number>
</root>
When you change values in the tableview the underlying array is updated but not your XML document. You need to update your XML document or recreate it from the array to hold edited values.

Can't retrieve data after storing to disk using NSKeyedArchiver

I'm storing a custom object in a dictionary which i'm then saving to disk using NSKeyedArchiver. In the same method where I save the data, I do a quick test at the end to make sure I can load the data but everything comes out null. Why is this happening? I've been following the instructions in this tutorial: http://cocoadevcentral.com/articles/000084.php
/** Interface of viewcontroller**/
#import "User.h"
#interface BWViewController : UIViewController
{
IBOutlet UITextField *userNameField;
IBOutlet UITextField *passwordField;
IBOutlet UILabel * loginStatus;
}
#property (nonatomic, copy) NSString *holdPassword, *holdUserName;
-(IBAction)signUpButton;
-(IBAction)loginButton;
-(NSString*) pathForDataFile;
-(void) saveDataToDisk:(User*) someUser;
-(User *) loadDataFromDisk:(NSString*) theKey;
#end
/**Implementation file **/
#import "BWViewController.h"
#interface BWViewController ()
#end
#implementation BWViewController
#synthesize holdPassword,holdUserName;
-(IBAction)signUpButton
{
User * firstUser = [[User alloc] init];
firstUser.userName = userNameField.text;
firstUser.password = passwordField.text;
/**To save to file**/
[self saveDataToDisk:firstUser];
loginStatus.text = #"Thanks for signing up";
}
-(NSString*) pathForDataFile
{
NSFileManager * fileManager = [NSFileManager defaultManager];
NSString *folder =#"~/Library/Application Support/signUpApp/";
folder = [folder stringByExpandingTildeInPath];
if([fileManager fileExistsAtPath:folder] == NO)
{
[fileManager createFileAtPath:folder contents:nil attributes:nil];
}
NSString *fileName = #"signUpApp.demo";
return [folder stringByAppendingPathComponent:fileName];
}
-(void) saveDataToDisk:(User*) someUser
{
User * savingUser = someUser;
NSString * path = [self pathForDataFile];
NSMutableDictionary * rootObject = [NSMutableDictionary dictionary];
[rootObject setValue:savingUser forKey:userNameField.text];
[NSKeyedArchiver archiveRootObject:rootObject toFile:path];
NSLog(#"saving user to disk %#", savingUser.userName);
/**Testing the load process**/
User * testLoadUser;
NSMutableDictionary *unRootObject;
unRootObject = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
testLoadUser = [unRootObject valueForKey:userNameField.text];
NSLog(#"Testing the unarchive %# and %#", testLoadUser.userName, testLoadUser.password);
}
/**User implementation file**/
#import "User.h"
#implementation User
#synthesize userName, password;
- (void) encodeWithCoder: (NSCoder *)coder
{
[coder encodeObject: userName forKey:#"username"];
[coder encodeObject: password forKey:#"password"];
}
- (id) initWithCoder: (NSCoder *)coder
{
if (self = [super init])
{
self.userName = [coder decodeObjectForKey:#"username"];
self.password = [coder decodeObjectForKey:#"password"];
}
return self;
}
#end
This looks like iOS code, but you're using a Mac OS X convention for determining the folder for the path. In iOS, you should use the Documents folder.
For iOS, the path for the filename in the Documents folder should be:
- (NSString*) pathForDataFile
{
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *path = [documentsPath stringByAppendingString:#"signUpApp.demo"];
return path;
}
I'd also suggest checking the return code from archiveRootObject method, to confirm whether the archive succeeded or not. No point in going further if that failed.
Also, in your old pathForDataFile, you're calling createFileAtPath:contents:attributes:, but I suspect you intended createDirectoryAtURL:withIntermediateDirectories:attributes:error:. The former is for creating a file, and the latter is for creating a folder. And your code is trying to create a folder.
Quick things you can check..
1) IS file is Actually created on the location ?
2) Is there any data (random chars) in that file ?
3) Is your code contains <NSCopying> protocol in your user.h class?
4) Make sure you don't have - (id) initWithCoder: (NSCoder *)coder and - (void) encodeWithCoder: (NSCoder *)coder in your header file.

Unable to save NSMutableArray of my class to file (IOS)

Can't find out why my code doesn't work. Please help someone.
I created my own class, implemented NSCoding protocol. Do not know what i miss or wrong.
Here is saving code
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory , NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *fileName = [documentsDirectory stringByAppendingPathComponent:#"Currency.plist"];
Item * item = [[Item alloc] init];
item.x = 3; item.y = 5; item.type = (TType) 3; item.isSelected = NO;
NSMutableArray * array = [[NSMutableArray alloc] init];
[array addObject:item];
[array fileName atomically:YES] // ( Doesn't Save the file ,returns NO);
Here is the code of my class
*.h*
#import <Foundation/Foundation.h>
enum TType
{
kNone = 0,
KFirst = 1,
....
};
#interface Item : NSObject <NSCoding>{
}
#property (nonatomic) int x;
#property (nonatomic) int y;
#property (nonatomic) enum TType type;
#property (nonatomic) BOOL isSelected;
#end
.m
#implementation Item
#synthesize x, y , type , isSelected;
#pragma mark NSCoding Protocol
- (void)encodeWithCoder:(NSCoder *)encoder;
{
[encoder encodeInt32:[self x] forKey:#"x"];
[encoder encodeInt32:[self y] forKey:#"y"];
[encoder encodeInt32:[self type] forKey:#"type"];
[encoder encodeBool:[self isSelected] forKey:#"isSelected"];
}
- (id)initWithCoder:(NSCoder *)decoder;
{
if ( ![super init] )
return nil;
[self setX:[decoder decodeInt32ForKey:#"x"]];
[self setY:[decoder decodeInt32ForKey:#"y"]];
[self setType:(TType)[decoder decodeInt32ForKey:#"color"]];
[self setIsSelected:[decoder decodeBoolForKey:#"isSelected"]];
return self;
}
#end
I think you'll find your answer at: objects conforming to nscoding will not writetofile
i.e. you can't serialize your Item class to a property list, since it isn't a property list object (NSString, NSData, NSArray, or NSDictionary).
See the documentation for writeToFile:atomically::
This method recursively validates that all the contained objects are property list objects before writing out the file, and returns NO if all the objects are not property list objects, since the resultant file would not be a valid property list.

Where can I find a generic game state singleton for Objective C?

I'm wanting to use, and store a game state singleton inside NSCoder, but I am finding it quite difficult to find a generic state management that saves and loads its data using the NSKeyedArchiver/NSCoder routines.
I'm wondering if someone can direct me to a good tutorial, or generic code for use as a game state singleton, with it saving/loading in NSCoder?
Thanks
I've been able to get a basic game state manager working from watching tutorials from 71squared.com
I use the SynthesizeSingleton.h from CocoaWithLove and am able to save and load states using NSCoder / archiving.
As it uses the singelton, I can reference it by writing:
GameStateManager *gsm = [GameStateManager sharedGameStateManager];
As below:
// GameStateManager.h file
#interface GameStateManager : NSObject
{
NSMutableArray *listOfPlayers;
}
#property (nonatomic, retain) NSMutableArray *listOfPlayers;
+ (GameStateManager *)sharedGameStateManager;
-(void)loadGameState;
-(void)saveGameState;
// GameStateManager.m
#import "GameStateManager.h"
#import "SynthesizeSingleton.h"
#import "Player.h"
#implementation GameStateManager
SYNTHESIZE_SINGLETON_FOR_CLASS(GameStateManager)
#synthesize listOfPlayers;
#pragma mark - Init
- (id)init
{
self = [super init];
if (self) {
// Initialization code here.
self.listOfPlayers = [NSMutableArray array];
}
return self;
}
#pragma mark - Memory management
-(void) dealloc
{
[super dealloc];
}
#pragma mark - Save/Load
-(void)saveGameState
{
NSLog(#"saveGameState");
// Set up the game state path to the data file that the game state will be save to
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *gameStatePath = [documentsDirectory stringByAppendingPathComponent:#"gameState.dat"];
// Set up the encoder and storage for the game state data
NSMutableData *gameData;
NSKeyedArchiver *encoder;
gameData = [NSMutableData data];
encoder = [[NSKeyedArchiver alloc] initForWritingWithMutableData:gameData];
// Archive the object
[encoder encodeObject:self.listOfPlayers forKey:#"playerObjects"];
// Finish encoding and write to disk
[encoder finishEncoding];
[gameData writeToFile:gameStatePath atomically:YES];
[encoder release];
}
-(void)loadGameState
{
NSLog(#"loadGameState");
// Set up the game state path to the data file that the game state will be save to
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *gameStatePath = [documentsDirectory stringByAppendingPathComponent:#"gameState.dat"];
NSMutableData *gameData;
NSKeyedArchiver *decoder;
gameData = [NSData dataWithContentsOfFile:gameStatePath];
// Check to see if the .dat file exists, and load contents
if (gameData)
{
decoder = [[[NSKeyedUnarchiver alloc] initForReadingWithData:gameData] retain];
self.listOfPlayers = [[[decoder decodeObjectForKey:#"playerObjects"] retain] autorelease];
NSLog(#"Returned %d players", [self.listOfPlayers count]);
for (Player *p in self.listOfPlayers)
{
NSLog(#"p.name = %#", p.fname);
}
// Finished decoding, release
[decoder release];
[decoder autorelease];
} else {
}
}
In my Player.h/.m file I do this:
#interface Player : NSObject
<NSCoding>
{
NSString *fname;
}
#property(nonatomic,retain) NSString *fname;
// Player.m file
#pragma mark - Encoding
-(id)initWithCoder:(NSCoder *)aDecoder
{
//[self initWithObject:[aDecoder decodeObject];
self = [super init];
if (self) {
// Initialization code here.
self.fname = [aDecoder decodeObjectForKey:#"fname"];
}
return self;
}
-(void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:self.fname forKey:#"fname"];
}
// ---
My follow-up questions are:
1) Am I meant to put a listOfPlayers in the GameStateManager?
--> Moved to a new question
2) In my encoder, I have to store each field individually, is it not possible to just store the actual object itself?
3) Does encoding of an object also encode all its children?
Lets say I have a Parent class (Player) and a Child class (Weapons). If I encode the Player object only, will it encode all the Weapons too automatically because they are linked?
Thanks.