I am writing an iPhone app – a client for some social network. The app support multiple accounts. Info about accounts are stored in a keyed archive.
A method used for saving:
- (void) saveAccounts {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *path = [paths objectAtIndex:0];
path = [path stringByAppendingPathComponent:#"accounts.bin"];
// NSMutableArray *accounts = ...;
[NSKeyedArchiver archiveRootObject:accounts toFile:path];
}
A method uses for reading:
- (NSMutableArray *) loadAccounts {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *path = [paths objectAtIndex:0];
path = [path stringByAppendingPathComponent:#"accounts.bin"];
NSMutableArray *restoredAccounts = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
return [restoredAccounts retain];
}
The method saveAccounts is used only if some account is added/modified/deleted. The method loadAccounts is used every time the app starts. There isn't any other code that access this file.
I and one of my testers get an issue. At some moment the starts to act like accounts.bin is missing. If loadAccounts returns nil, the app offers to add an account. After I enter an account (the app must call saveAccounts), the app works normally, but when I launch it again, it asks me to add account again. The only solutions is too reinstall the app to iPhone, after reinstall it works for some time with any troubles.
We (I and my tester who get an issue) use iPhone 3G with 3.1.2.
An another tester who didn't experience this issue on his iPhone 3GS with 3.1.2.
Any ideas why this file disappears?
update
I found bug in my code. There was a code that deletes whole Document directory. Because this part of a code is a remote server related, it was hard to trace this code. Bug appeared under very rare conditions only.
Now the bug is found, and the code is corrected. wkw's answer didn't solved my problem, but it forced me to dig deeper. Thank you!
How about -- as a debugging device --verifying the contents of your Documents directory in loadAccounts or at least whenever the unarchiver returns nil. There's some code below to get the names of files in a directory. Set a breakpoint and just dump the NSArray to view the items.
If you can see that the file exists in the Docs dir, then your problem is elsewhere. Perhaps it did not successfully archive. check the return value on the call to archive the data:
if( ! [NSKeyedArchiver archiveRootObject:accounts toFile:path] ){
NSLog(#"Oooops! account data failed to write to disk");
}
Get names of files in directory:
- (NSArray*) directoryContentsNames:(NSString*) directoryPath {
NSArray* result;
{
result = [[NSFileManager defaultManager]
directoryContentsAtPath: directoryPath];
}
if( result && [result count] > 0 ){
NSMutableArray *items = [[[NSMutableArray alloc] init] autorelease];
for( NSString *name in result ){
if( ! [name isEqualToString:#".DS_Store"] )
[items addObject: name];
}
result = items;
}
return result;
}
Perhaps NSUserDefaults might be easier to use?
Also, is there a reason to use Keyed(Un)Archiver instead of NSArray's writeToFile?
if( ! [accounts writeToFile:path atomically:YES] )
; // do something since write failed
and to read the file in:
NSMutableArray *accounts = [[NSArray arrayWithContentsOfFile:path] mutableCopy];
Related
In the program I use this method to create a file in which I can save:
-(NSString*) saveFilePath{
NSString* path = [NSString stringWithFormat:#"%#%#",
[[NSBundle mainBundle] resourcePath],
#"savingfile.plist"];
return path;}
Then I used a button to initiate the process of putting the data into the file (I first put it all into an array so that it would be easier.):
- (IBAction)save
{
NSMutableArray *myArray = [[NSMutableArray alloc] init];
[myArray addObject:name.text];
[myArray addObject:position.text];
[myArray addObject:cell.text];
[myArray addObject:office.text];
[myArray addObject:company.text];
[myArray writeToFile:[self saveFilePath] atomically:YES];
}
Finally, I loaded the information back into the textfields in the - (void)viewDidAppear method.
- (void)viewDidAppear:(BOOL)animated
{
NSMutableArray* myArray = [NSMutableArray arrayWithContentsOfFile:[self saveFilePath]];
name.text = [myArray objectAtIndex:0];
position.text = [myArray objectAtIndex:1];
cell.text = [myArray objectAtIndex:2];
office.text = [myArray objectAtIndex:3];
company.text = [myArray objectAtIndex:4];
}
For some reason, on the simulator it's working perfectly on the simulator, but not working at all when I try to run on my physical iPhone.
I think you're trying to save to a location that's read-only on iOS. It works on the simulator because the simulator doesn't totally replicate the sandboxing environment on the actual hardware.
Rather than saving to the resourcesPath you should be saving your files to the Documents directory (or the cache directory, if appropriate).
You can get a path to the documents directory as follows:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
There's more information at this question here: How to save file in resource folder of an app in Objective-c
I have an app that is supposed to save to a file and later on load it. Now, I have not had ANY problems what so ever on ios 4, so this is perplexing. This has happened on all my apps saving and loading.
Heres the code:
- (NSString *)pathOfFile{
NSArray *paths =NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsFolder = [paths objectAtIndex:0];
return [documentsFolder stringByAppendingFormat:#"awesome.plist"];
}
Later in in the app...
[array writeToFile:[self pathOfFile] atomically:YES];
And then when I attempt to load it...
if ([[NSFileManager defaultManager] fileExistsAtPath:[self pathOfFile]]) {
NSMutableArray *array = [[NSMutableArray alloc] initWithContentsOfFile:filepath];
achi.text = [array objectAtIndex:0];
}
My app actually just skips over the if statement (Meaning that it can't find the file I think).
Please help, and if you have different methods of saving files, I would be glad to hear to hear them.
Your - (NSString *)pathOfFile method is wrong. It should be:
- (NSString *)pathOfFile
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsFolder = [paths objectAtIndex:0];
return [documentsFolder stringByAppendingPathComponent:#"awesome.plist"];
}
In your -(NSString *)pathOfFile method, don't use stringByAppendingFormat:. When working with file paths, you should instead use stringByAppendingPathComponent:, as it will ensure that the appropriate slash characters are added (or removed, if there are too many):
return [documentsFolder stringByAppendingPathComponent:#"awesome.plist"];
The comment to my question was what solved the problem, but as I can't give him the correct answer, I'll just write paste his answer here:
Did you make sure the directory is there? Sometimes that Documents directory must be created.
in Objective C how would I get a list of ALL files on the iPhone?
is this even possible or can I only get files from a certain directory or known path?
Not without jail-breaking your phone. All apps live in a sandbox and can only see certain files.
No you can just get files within your sandbox. And to display your current directory use the following code :
- (NSString *) returnFilePath {
NSArray *pathArray =
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
return [pathArray objectAtIndex:0];
}
I am not sure why people are saying that your app can only see certain files inside of your sandbox. As of iOS 2.x (the last time I tried something like this), you can use NSFileManager to list files in almost any directory. Here is a little code to get all of the names of all files in a specific directory.
- (NSArray *)allFiles:(NSString *)aPath
NSMutableArray * listing = [NSMutableArray array];
NSArray * fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:aPath error:nil];
if (!fileNames) return listing;
for (NSString * file in fileNames) {
NSString * absPath = [aPath stringByAppendingPathComponent:file];
BOOL isDir = NO;
if ([[NSFileManager defaultManager] fileExistsAtPath:absPath isDirectory:&isDir]) {
if (isDir) {
[listing addObject:absPath];
[listing addObjectsFromArray:[self allFiles:absPath]];
} else {
[listing addObject:absPath];
}
}
}
return listing;
}
This is an example of a very simple recursive function. It could, of course be modified to work more efficiently with blocks as callbacks or even incorporate NSOperation.
I am getting slightly frustrated with the DropBox API. It is supposed to be all simple and straight forward, but I have yet to come a across a simple and plain explanation of how to do a simple sync.
I followed all the instruction you can find in the readme which comes withe DropBox API. To test the whole thing, I have created two buttons to download and upload a file from or to my DropBox. The files are found in my app documents folder.
This works splendidly:
-(void) DBupload:(id)sender
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0]; // Get documents directory
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:#"MyExample.txt"];
// NSError *error;
[self.restClient uploadFile:#"MyExample.txt" toPath:#"/MyExamplePath" fromPath:filePath];
}
-(void) DBdownload:(id)sender
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0]; // Get documents directory
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:#"MyExample.txt"];
NSError *error;
[self.restClient loadFile:#"/myExamplePath/MyExample.txt" intoPath:filePath];
}
However, I am now wondering how to achieve a simply sync. Right now, I can manually upload and download. But what I need in order to sync is to:
find out if the MyExample.txt in my App's folder or in my DropBox folder is more recent
if the txt in the App's folder is more recent: drop it into dropbox (overriding the old), i.e. call my DBupload method
if the txt in the drop box is more recent: download it into the apps folder, i.e. call my DBdownload method
Perhaps I am just too dumb, but does dropBox detail somewhere how to achieve this rather simple and straight forward task?
I know that there is this but it doesn't really give any code samples.
Thanks for any suggestions.
EDIT
OK, so I figured that my first step is to find out the last modified date of MyExample.txt which is found in the dropBox.
I wrote a wonderful method called DBsync in which I simply put this command:
-(void) DBsync
{
[self.restClient loadMetadata:#"/myExamplePath"];
}
This calls the following method which gets the metadata. This was a suggested answer to this post, and I commented it a bit out so as to make it plain what is happening (if there are more dumb people like myself:
- (void)restClient:(DBRestClient*)client loadedMetadata:(DBMetadata*)metadata {
NSLog(#"restClient:loadedMetadata function called");
NSEnumerator *e= [metadata.contents objectEnumerator]; // indexes files in dropbox?
DBMetadata *dbObject; // loads metadate we need, e.g. lastModifiedDated
int numberOfFiles = [metadata.contents count]; // counts files in DropBox - I guess we don't really need this
NSLog(#"numberOfFiles %i", numberOfFiles);
while ((dbObject = [e nextObject])) { // this goes through every single file in the DropBox
if (!dbObject.isDirectory) { // this determines whether we are talking about a file or a folder
NSString *fileName = [dbObject.path lastPathComponent]; // this puts the name of the last component, e.g. in /MyExamplePath/MyExample.txt = MyExample.txt into fileName
NSLog(#"File which is currently being checked: %#", fileName);
if ([fileName isEqualToString:#"MyExample.txt"]) {
NSLog(#"Found it: %#", fileName);
NSLog(#"Date last modified: %#", dbObject.lastModifiedDate);
/* to do: call dbupload if dbObject.lastModifiedDate > than your local file*/
}
}
}
}
I will post the next step once I managed to do so...
I think what you are looking for is the loadmetadata method. Here is an untested example:
- (void)restClient:(DBRestClient*)client loadedMetadata:(DBMetadata*)metadata {
NSEnumerator *e= [metadata.contents objectEnumerator];
DBMetadata *dbObject;
numberOfFiles = [metadata.contents count];
while ((dbObject = [e nextObject])) {
if (!dbObject.isDirectory) {
NSString *fileName = [dbObject.path lastPathComponent];
if (![fileName isEqualToString:#"MyExample.txt"]) {
/* call dbupload if dbObject.lastModifiedDate > than your local file*/
}
}
}
You don't need an enumerator, just use the good old for... loop ;)
- (void)restClient:(DBRestClient *)client loadedMetadata:(DBMetadata *)metadata {
for (DBMetadata * child in metadata.contents) {
if (!child.isDirectory) {
NSString *fileName = [dbObject.path lastPathComponent];
if (![fileName isEqualToString:#"MyExample.txt"]) {
/* call dbupload if dbObject.lastModifiedDate > than your local file*/
}
}
}
}
I am building an add-on to my app where the user can search for an item in a list that is pre-populated with data from a .plist file. It is an NSDictionary. If the term, the user searched for, does not exist, the user can tap a + button and add it so it is there the next time.
First of I thought it would be as easy as using the NSUserDefaults, but a few problems arises.
To have the list included I must place it in the bundle, but if it is there I can not add new key/value pairs to it. This I can only do with files situated in the Documents folder.
So I guess I have to bundle the plist, then on first run I'll move it to the documents folder and access it there.
This opens up the problem when I need to update the app, I guess it will overwrite the values the user put in.
Is there a secure, easy-understandable, right way to achieve the functionality I describe?
Thanks for any help given:)
Edit: **** the actual approach, as suggested by TheSquad and TomH *****
+ (NSMutableDictionary*) genericProducts {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [documentPaths objectAtIndex:0];
NSString *documentPlistPath = [documentsDirectory stringByAppendingPathComponent:#"GenericProducts.plist"];
NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
NSString *bundlePlistPath = [bundlePath stringByAppendingPathComponent:#"GenericProducts.plist"];
if([fileManager fileExistsAtPath:documentPlistPath]){
NSMutableDictionary *documentDict = [NSMutableDictionary dictionaryWithContentsOfFile:documentPlistPath];
return documentDict;
} else {
NSError *error;
BOOL success = [fileManager copyItemAtPath:bundlePlistPath toPath:documentPlistPath error:&error];
if (success) {
NSMutableDictionary *newlySavedDict = [NSMutableDictionary dictionaryWithContentsOfFile:documentPlistPath];
return newlySavedDict;
}
return nil;
}
}
And for adding a new product to the list:
+ (void) addItemToGenericProducts:(NSString*) newProduct {
NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [documentPaths objectAtIndex:0];
NSString *documentPlistPath = [documentsDirectory stringByAppendingPathComponent:#"GenericProducts.plist"];
NSMutableDictionary *documentDict = [NSMutableDictionary dictionaryWithContentsOfFile:documentPlistPath];
[documentDict setObject:newProduct forKey:[MD5Checksum cheksum:newProduct]];
[documentDict writeToFile:documentPlistPath atomically:YES];
}
I had the same thoughts with my sqlite database...
I end up doing exactly that, copy the bundled file into documents in order to be able to modify it.
What I have done is checking at each startup if the file exist, if it does not, copy it.
If you do an update of your App, the documents folder will not be touch, this means the copied file from the previous version will still be present.
The only issue is that if you want your plist to be upgraded you will have to handle that in your application. If you have to do so I suggest you use the NSUserDefault to check if a previous version of the app existed before...
The contents of the documents directory is not altered when an application is updated.
The contents of the documents directory are deleted when the user deletes the app.
When the app is run the first time write a flag to NSUserDefaults. On subsequent runs of the app, check for existence of the flag. (alternatively, you can just check for existence of the plist in he documents directory)