I'm working on an app that lets users record voice (among other things) to the Documents directory of the app. But when I'm recording the voice, I'm recording to the caches directory of the app and then after the user says "Okay, save this one", then I'm coping it to the Documents directory. So far all these work. But if I try to delete the data file in cache, or when I try to move it, I get problems.
So my question is, shall I just leave the data in cache so that iOS will handle it or do I need to manually delete the files in cache. If so how would I go about doing it. This is the code I have so far (which doesn't work)
NSFileManager *fm = [NSFileManager defaultManager];
NSString *directory = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSError *error = nil;
BOOL success = [fm removeItemAtPath:[NSString stringWithFormat:#"%#%#", directory, currentEntry.audioFileURL] error:&error];
if (!success || error) {
// it failed.
NSLog(#"it failed to delete!!! %# %#", error, [error userInfo]);
} else {
NSLog(#"Deleted... yipee... !!!");
}
I think the problem is that your path is not correct. Always use the
- (NSString *)stringByAppendingPathComponent:(NSString *)aString
method of NSString.
You can try printing out the path you get now (maybe a backslash is missing), but anyway you should use the method I described.
UPD: Another thing is that NSCachesDirectory is actually never cleaned up. Use NSTemporaryDirectory() if you want automatic cleaning.
Related
I have file - of arbitrary type including Core Data repositories - that I need to keep around and not have iOS delete them. Usually, I do not want iCloud to back these files up. Where should I save them?
Saving files locally changed from iOS5.0 and earlier, 5.0.1, and 5.1 and newer primarily to the address iCloud backup issues. There are two Apple source documents (the File System Programming Guide, and QA1719) that together provide the information supporting the following:
iOS 5.0
Files should be saved in the "Caches" directory, as there is no way to prevent backups if they are stored in the Documents folder. Note that the system may remove these files (see QA1719), so you would need the ability to recreate each as needed. To find the caches directory, use this:
[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]
iOS 5.0.1
Files should be saved in '/Library/Application Support' (FSP, page 15), which can be better accessed via:
[NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) lastObject]
My experience is that this directory doesn't always exist, and thus you may need to create it:
- (NSString *)applicationAppSupportDirectory
{
return [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) lastObject];
}
NSFileManager *manager = [NSFileManager defaultManager];
NSString *appSupportDir = [self applicationAppSupportDirectory];
if(![manager fileExistsAtPath:appSupportDir]) {
__autoreleasing NSError *error;
BOOL ret = [manager createDirectoryAtPath:appSupportDir withIntermediateDirectories:NO attributes:nil error:&error];
if(!ret) {
NSLog(#"ERROR app support: %#", error);
exit(0);
}
}
Files saved to this directory (or subdirectories) need an extended attribute to tell iCloud not to back them up (see QA1719).
PS: I have not found a way to set the Deployment target to this release, if there is a way please leave a comment.
iOS 5.1
Files (or folders of files) should be located in the 'Application Support' folder as described above. To prevent iCloud from backing up use:
[URL setResourceValue: [NSNumber numberWithBool: YES] forKey:NSURLIsExcludedFromBackupKey error:&error]
as described in QA1719. Note that you can apply this key to a directory to prevent its contents from being backed up. The complete method from QA1719:
- (BOOL)addSkipBackupAttributeToItemAtURL:(NSURL *)URL
{
assert([[NSFileManager defaultManager] fileExistsAtPath: [URL path]]);
NSError *error = nil;
BOOL success = [URL setResourceValue: [NSNumber numberWithBool: YES]
forKey: NSURLIsExcludedFromBackupKey error: &error];
if(!success){
NSLog(#"Error excluding %# from backup %#", [URL lastPathComponent], error);
}
return success;
}
My app is using the NSDocumentDirectory to save images in it, I just wanna ask if its the safe way to save images(100 maximum). I have read several thread & questions with answers about it, though I dont know which to follow.Some say that its okay to save there. Some say I shouldnt use NSDocumentDirectory for saving, because it will be back-up by the iCloud. So where can I save it that when the user exit the app then run the app again, then images should still be there?. I dont know much about the tmp directory or cache directory. But if its either one of the 2 that I should use, How can I use them in my code here:
NSArray *paths = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask ,YES );
NSString *documentsDir = [paths objectAtIndex:0];
NSString *savedImagePath = [documentsDir stringByAppendingPathComponent:[NSString stringWithFormat:#"Images%d.png", i]];
ALAssetRepresentation *rep = [[info objectAtIndex: i] defaultRepresentation];
UIImage *image = [UIImage imageWithCGImage:[rep fullResolutionImage]];
//----resize the images
image = [self imageByScalingAndCroppingForSize:image toSize:CGSizeMake(256,256*image.size.height/image.size.width)];
NSData *imageData = UIImagePNGRepresentation(image);
[imageData writeToFile:savedImagePath atomically:YES];
Thank you so much for the help.
The tmp and cache directories are periodically cleaned up by iOS. If the images are for general use, use the camera roll as the other two answers suggest. However if these images are intended just for the scope of your app, you can still safely store them in the Documents directory, you just have to include an "exclude from iCloud backup" function call to each file after saving, in order to prevent Apple rejecting your app for using too much iCloud space. Of course there's a trade-off, disabling this means the user will lose their photos anyway should they delete the app or get another device(etc), but this caveat is preferable to not getting the App on the store at all.
To disable iCloud backup on a file, there's two methods for iOS versions > 5.0:
UPDATE! MERGED BOTH METHODS INTO A SINGLE FUNCTION THAT AUTOMATICALLY HANDLES iOS VERSION:
#include <sys/xattr.h> // Needed import for setting file attributes
+(BOOL)addSkipBackupAttributeToItemAtURL:(NSURL *)fileURL {
// First ensure the file actually exists
if (![[NSFileManager defaultManager] fileExistsAtPath:[fileURL path]]) {
NSLog(#"File %# doesn't exist!",[fileURL path]);
return NO;
}
// Determine the iOS version to choose correct skipBackup method
NSString *currSysVer = [[UIDevice currentDevice] systemVersion];
if ([currSysVer isEqualToString:#"5.0.1"]) {
const char* filePath = [[fileURL path] fileSystemRepresentation];
const char* attrName = "com.apple.MobileBackup";
u_int8_t attrValue = 1;
int result = setxattr(filePath, attrName, &attrValue, sizeof(attrValue), 0, 0);
NSLog(#"Excluded '%#' from backup",fileURL);
return result == 0;
}
else if (&NSURLIsExcludedFromBackupKey) {
NSError *error = nil;
BOOL result = [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:&error];
if (result == NO) {
NSLog(#"Error excluding '%#' from backup. Error: %#",fileURL, error);
return NO;
}
else { // Succeeded
NSLog(#"Excluded '%#' from backup",fileURL);
return YES;
}
} else {
// iOS version is below 5.0, no need to do anything
return YES;
}
}
If your app must support 5.0, then unfortunately your only option is to save those photos in the Caches directory, which means they won't be backed up (this not causing an App Store rejection for that reason), but whenever the storage watchdog decides it's time to clean the Caches folder, you'll lose those photos. Not an ideal implementation at all, but such is the nature of the beast in 5.0, where Apple added in Backup exclusion as an afterthought.
EDIT: Forgot to answer the 'how to save to the tmp/cache directory' part of the question. If you do decide to go down that path:
Saving to tmp:
NSString *tempDir = NSTemporaryDirectory();
NSString *savedImagePath = [tempDir stringByAppendingPathComponent:[NSString stringWithFormat:#"Images%d.png", i]];
(note that this won't appear to have any effect in the simulator, but it works as expected on device)
Saving to Cache:
NSString *cacheDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask,YES)lastObject];
NSString *savedImagePath = [cacheDir stringByAppendingPathComponent:[NSString stringWithFormat:#"Images%d.png", i]];
If you want the user to be able to use the images in other apps or view them along with their photos, use the photo album as Mike D suggest. If the files are something you generate locally for use with your app only, then you should probably use the documents directory. You can expose the documents directory to iTunes with the info.plist option "Application supports iTunes file sharing" which will allow the user to add or delete files through iTunes, but the files will not be exposed to other apps
You are saving scaled images so they are really only useful for your game. They are not going to be very large and will not take up much space. You could save them in the Library directory for the app and avoid the whole iCloud thing, as it doesn't sound like there is any reason to back them up. Also, saving the the Library avoid the possibility of the user deleting them, if for some other reason you have iTunes sharing turned on.
Update: code for saving to the app Library directory
- (void)saveSequences:(NSMutableDictionary*)sequences
{
NSArray *path = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString *libDirectory = [path objectAtIndex:0];
NSString *settingsPath = [libDirectory stringByAppendingPathComponent:#"userSequences.plist"];
NSLog(#"settingsPath %#", settingsPath);
[sequences writeToFile:settingsPath atomically:YES];
}
// The code below gets the path to a named directory in the 'Documents' folder - and if it doesn't exist, creates it. Adjust it to use the Library path, if you decide to go that route.
- (NSString *)getDirectoryBySequenceName:(NSString *)sequenceName
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString * documentDirectory = [paths objectAtIndex:0];
NSString * sequenceDirectory = [documentDirectory stringByAppendingPathComponent:sequenceName];
NSError *error;
BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:sequenceDirectory
withIntermediateDirectories:YES
attributes:nil error:&error];
if (!success) {
NSLog(#"Error creating data path: %#", [error localizedDescription]);
}
return sequenceDirectory;
}
Depending on the purpose of your app, you could save it to the photos app (UIImageWriteToSavedPhotosAlbum(image, self, nil, nil) I think, Apple reference). Saving here, or in the documents directory (or any sub folder), will allow the user backup those images to iCloud or iTunes, if the user chooses too and/or if they have set up iCloud.
Since you state the images need to persist between launches, the temp or cache directory get emptied when the application is removed from memory, maybe sooner (the O/S decides).
More about the iOS file system.
I am saving Images in NSCachesDirectory in a App. At the end of app execution i would like to clear all temporary cache. Is there a way to force clear all cache on application exit. I do realise the local folder keeps cache for 3 days..but my requirement is to force clear the Cache. Thx
This code should do the trick, just substitute the name of your cache directory...
NSString *cacheDirectoryName = [self getCacheDirectoryName];
NSArray *items = [fileManager directoryContentsAtPath:cacheDirectoryName];
for (NSString *item in items)
{
NSString *path = [cacheDirectoryName stringByAppendingPathComponent:item];
NSError *error = nil;
[fileManager removeItemAtPath:path error:&error];
[error release];
}
Then call the code in your AppDelegate applicationWillTerminate method.
In my app I sometimes need to rebuild and repopulate database file. SQLite databse is created and managed by CoreData stack.
What I'm trying to do is drop the file and then simply recreate persistentStoreCoordinator object.
It works under simulator but not on device, where I'm getting such an error:
NSFilePath = "/var/mobile/Applications/936C6CC7-423A-46F4-ADC0-7184EAB0CADD/Documents/MYDB.sqlite";
NSUnderlyingException = I/O error for database at /var/mobile/Applications/936C6CC7-423A-46F4-ADC0-7184EAB0CADD/Documents/MYDB.sqlite. SQLite error code:1, 'table ZXXXX already exists';
I cannot find the cause of this in any way. It indicates two different problems - Cocoa error 256 indicates that file does not exist or is not readable. But file IS created after creating persistenStoreCoordinator, although it's empty, but after executing some queries it disappears.
Second message indicating attempt to create alredy existing table is quite strange in that case.
I'm quite confused and cannot get the point what's going on here. My code looks like this:
NSString *path = [[WLLocalService dataStorePath] relativePath];
NSError *error = nil;
WLLOG(#"About to remove file %#", path);
[[NSFileManager defaultManager] removeItemAtPath: path error: &error];
if (error != nil) {
WLLOG(#"Error removing the DB: %#", error);
}
[self persistentStoreCoordinator];
WLLOG(#"Rebuild DB result %d", [[NSFileManager defaultManager] fileExistsAtPath: path]);
After this code is exectued, DB file exists but is empty. When then first query (and all following) is executed, it gives me the error above and file disappears.
Does anybody has an idea what's wrong with it?
Big thanks for pointing me the right way!
The Core Data stack does not like you removing the file under it. If you are wanting to delete the file you should tear down the stack, delete the file and then reconstruct the stack. That will eliminate the issue.
Part of the problem is that the stack keeps a cache of the data that is in the file. When you remove the file you don't have a way to clear that cache and you are then putting Core Data into an unknown and unstable state.
You can try telling the NSPersistentStoreCoordinator you are removing the file with a call to -removePersistentStore:error: and then adding the new store with a call to -addPersistentStoreWithType:configuration:URL:options:error:. I am doing that currently in ZSync and it works just fine.
I use the following method -resetApplicationModel in my app delegate and it works fine for me.
You may not need the kApplicationIsFirstTimeRunKey user default, but I use it to test whether to populate the Core Data store with default settings in a custom method called -setupModelDefaults, which I also call from -applicationDidFinishLaunching: if the first-time run flag is YES.
- (BOOL) resetApplicationModel {
// ----------------------
// This method removes all traces of the Core Data store and then resets the application defaults
// ----------------------
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:YES] forKey:kApplicationIsFirstTimeRunKey];
NSLog(#"Turned ON the first-time run flag...");
NSError *_error = nil;
NSURL *_storeURL = [NSURL fileURLWithPath: [[self applicationDocumentsDirectory] stringByAppendingPathComponent: #"MyAppSQLStore.sqlite"]];
NSPersistentStore *_store = [persistentStoreCoordinator persistentStoreForURL:_storeURL];
//
// Remove the SQL store and the file associated with it
//
if ([persistentStoreCoordinator removePersistentStore:_store error:&_error]) {
[[NSFileManager defaultManager] removeItemAtPath:_storeURL.path error:&_error];
}
if (_error) {
NSLog(#"Failed to remove persistent store: %#", [_error localizedDescription]);
NSArray *_detailedErrors = [[_error userInfo] objectForKey:NSDetailedErrorsKey];
if (_detailedErrors != nil && [_detailedErrors count] > 0) {
for (NSError *_detailedError in _detailedErrors) {
NSLog(#" DetailedError: %#", [_detailedError userInfo]);
}
}
else {
NSLog(#" %#", [_error userInfo]);
}
return NO;
}
[persistentStoreCoordinator release], persistentStoreCoordinator = nil;
[managedObjectContext release], managedObjectContext = nil;
//
// Rebuild the application's managed object context
//
[self managedObjectContext];
//
// Repopulate Core Data defaults
//
[self setupModelDefaults];
return YES;
}
You can keep a "clean" copy of your sqlite database as part of the application bundle, then just copy over the version in the documents directory whenever you'd like to refresh the database.
Here's some code from an App that does something similar (although this version will not copy over and existing db):
// Check for the existence of the seed database
// Get the path to the documents directory and append the databaseName
NSString* databasePath = [[self applicationDocumentsDirectory] stringByAppendingPathComponent: kDatabaseName];
NSFileManager* fileManager = [NSFileManager defaultManager];
if ( ![fileManager fileExistsAtPath: databasePath] )
{
NSString* databasePathFromApp = [[[NSBundle mainBundle] resourcePath]
stringByAppendingPathComponent: kDatabaseName];
[fileManager copyItemAtPath: databasePathFromApp
toPath: databasePath
error: nil];
}
[fileManager release];
I cache the images to the document directory of my app using the following code.
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *saveDirectory = [paths objectAtIndex:0];
But even after I close the application, its still there. How can I clear it when I lose my app. I am doing all this in my simulator.
I would implement applicationWillTerminate: in my application delegate and remove the cache files there. Or better yet, as suggested by Vladimir, save them in a temporary directory and let the OS clean them up when needed.
- (void)applicationWillTerminate:(UIApplication *)app
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error;
NSArray *cacheFiles = [fileManager contentsOfDirectoryAtPath:saveDirectory error:error];
for (NSString *file in cacheFiles) {
error = nil;
[fileManager removeItemAtPath:[saveDirectory stringByAppendingPathComponent:file] error:error];
/* handle error */
}
}
If you do not want your cached images to be preserved after application is closed better save them to temporary directory - they will be removed automatically.
If you want to manually remove the files you must store the paths for them and use the following NSFileManager function:
- (BOOL)removeItemAtPath:(NSString *)path error:(NSError **)error
Edit: sorry, I appeared to be wrong here about automatic deleting. Here's a quote from Developing Guide:
Use this directory to write temporary files that you do not need to persist between launches of your application. Your application should remove files from this directory when it determines they are no longer needed. (The system may also purge lingering files from this directory when your application is not running.)
NSString *file;
NSDirectoryEnumerator *dirEnum = [[NSFileManager defaultManager] enumeratorAtPath:saveDirectory];
NSError* err;
while (file = [dirEnum nextObject]) {
err = nil;
[[NSFileManager defaultManager] removeItemAtPath:[saveDirectory stringByAppendingPathComponent:file] error:&err]];
if(err){
//print some errror message
}
}
Use the temporary directory path as specified in this question:
How can I get a writable path on the iPhone?