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.
Related
I want to load a webpage when user connected to network and store it offline(including with images/resources). If the user not connected to any network then i should load the previously stored webpage. I have tried NSData dataWithContentsOfURL:(NSURL *)url and NSString stringWithContentsOfURL but these stores only html content not the resources.
Thanks in advance.
You can do that with ASIHttpRequest. If you do not want to to use that project (it is no longer active) you can look into the code and what it does. Look at "how to cache a whole web page with images in iOS" for more info as well.
I think the simple solution is this - "Safari Client-Side Storage and Offline Applications Programming Guide", https://developer.apple.com/library/archive/documentation/iPhone/Conceptual/SafariJSDatabaseGuide/Introduction/Introduction.html
Only if you are making an app with HTML5 and webview, didn't test this method yet so far, so it might work.
Write this data into file using:
-(void)writeDataToFile:(NSString*)filename
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
if(filename==nil)
{
DLog(#"FILE NAME IS NIL");
return;
}
// the path to write file
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:[NSString stringWithFormat: #"%#",filename]];
/*NSData *writeData;
writeData=[NSKeyedArchiver archivedDataWithRootObject:pArray]; */
NSFileManager *fm=[NSFileManager defaultManager];
if(!filePath)
{
//DLog(#"File %# doesn't exist, so we create it", filePath);
[fm createFileAtPath:filePath contents:self.mRespData attributes:nil];
}
else
{
//DLog(#"file exists");
[self.mRespData writeToFile:filePath atomically:YES];
}
NSMutableData *resData = [[NSMutableData alloc] init];
self.mRespData=resData;
[resData release];
}
and load it next time.
I don't know if there is one-line-solution like myWebView.cache4Offline = YES; , but I fear as long as you don't have access to the website's code (i.e. if you want to make any website available offline inside your app), you have to program this on your own. Thinking about it, it doesn't seem so difficult:
Scan the html string for image urls (and everything else you need)
Download those resources from the internet using NSData dataWithContentsOfURL (maybe a little annoying, because of relative/absolute URLs)
Save data to file with NSData writeToFile:options:error:
Replace URL in HTML with filePath from 3. (OR, better use a convention for converting their URLs in your file-URLs)
Hope it helps
Scroll down to EDIT 1. This top bit is irrelevant now.
This is my code:
NSMutableDictionary *dictionary = [self.media objectAtIndex:index];
NSLog(#"dictionary: %#", dictionary);
NSString *originalImagePath = [dictionary objectForKey:#"OriginalImage"];
NSLog(#"path: %#", originalImagePath);
UIImage *originalImage = [UIImage imageWithContentsOfFile:originalImagePath];
NSLog(#"original image: %#", originalImage);
return originalImage;
And my NSLog:
dictionary: {
IsInPhotoLibrary = 1;
MediaType = 0;
OriginalImage = "/var/mobile/Applications/5E25F369-9E05-4345-A0A2-381EDB3321B8/Documents/Images/E9904811-B463-4374-BD95-4AD472DC71A6.jpg";
}
path: /var/mobile/Applications/5E25F369-9E05-4345-A0A2-381EDB3321B8/Documents/Images/E9904811-B463-4374-BD95-4AD472DC71A6.jpg
original image: (null)
Any ideas why this might be coming out as null, despite everything appearing to be in place?
EDIT:
This is the code to write the image to file:
+(NSString *)writeImageToFile:(UIImage *)image {
NSData *fullImageData = UIImageJPEGRepresentation(image, 1.0f);
NSString *path = [NSHomeDirectory() stringByAppendingPathComponent:#"Documents/Images/"];
NSString *name = [NSString stringWithFormat:#"%#.jpg", [JEntry generateUuidString]];
NSString *filePath = [path stringByAppendingPathComponent:name];
[fullImageData writeToFile:filePath atomically:YES];
NSLog(#"original image 2: %#", [UIImage imageWithContentsOfFile:filePath]);
}
This NSLog also comes out as null. So the problem lies here, most likely. Any ideas?
EDIT 2:
So, back-tracing even more now, and I've realised that it's because this fails:
[fullImageData writeToFile:filePath atomically:YES];
You can return a bool on that line, telling if it was successful or not, and it's returning NO for me. Any ideas why this might be?
EDIT 3:
The image that gets passed in is NULL. Trying to figure out where that's gone wrong now.
Almost certainly your file does not exist. Modify your code as follows to log if a file exists or not:
NSString *originalImagePath = [dictionary objectForKey:#"OriginalImage"];
NSLog(#"path: %#", originalImagePath);
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:originalImagePath];
NSLog(#"file exists: %d", fileExists);
Possible Reasons:
The file at originalImagePath does not exist.
UIImage can not initialize an image from the specified file, because the data is corrupted (maybe the data is empty or incomplete)
the file can not be accessed because of iOS file permissions (i.e. when accessing files beyond the app sandbox)
Have you checked that the image file is really placed on path in dictionary? You may use the iExplorer utility for that.
Also pay attention to the file name case, so it's extension is 'jpg' not 'JPG'.
Finally you should check, whether it's a valid image file, by opening it with some image viewer.
are you sure that the jpg file "E9904811-B463-4374-BD95-4AD472DC71A6.jpg" is there in that folder?
try to open the app file installed in the iPhone/ipad simulator clicking on yourFile.app
with ctrl key and choose to open package contents the open your folder images...
to get a fast link to your documents folder:
NSArray *dirPaths;
NSString *docsDir;
dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
docsDir = [dirPaths objectAtIndex:0];
NSLog(#"doc:%#",docsDir);
your app should be in its parent folder
I'm writing an app that copies some contents of the bundle into the applications Document's directory, mainly images and media. I then access this media throughout the app from the Document's directory.
This works totally fine in the Simulator, but not on the device. The assets just come up as null. I've done NSLog's and the paths to the files look correct, and I've confirmed that the files exist in the directory by dumping a file listing in the console.
Any ideas? Thank you!
EDIT
Here's the code that copies to the Document's directory
NSString *pathToPublicationDirectory = [NSString stringWithFormat:#"install/%d",[[[manifest objectAtIndex:i] valueForKey:#"publicationID"] intValue]];
NSString *manifestPath = [[NSBundle mainBundle] pathForResource:#"content" ofType:#"xml" inDirectory:pathToPublicationDirectory];
[self parsePublicationAt:manifestPath];
// Get actual bundle path to publication folder
NSString *bundlePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:pathToPublicationDirectory];
// Then build the destination path
NSString *destinationPath = [documentsDirectory stringByAppendingPathComponent:[NSString stringWithFormat:#"%d", [[[manifest objectAtIndex:i] valueForKey:#"publicationID"] intValue]]];
NSError *error = nil;
// If it already exists in the documents directory, delete it
if ([fileManager fileExistsAtPath:destinationPath]) {
[fileManager removeItemAtPath:destinationPath error:&error];
}
// Copy publication folder to documents directory
[fileManager copyItemAtPath:bundlePath toPath:destinationPath error:&error];
I am figuring out the path to the docs directory with this method:
- (NSString *)applicationDocumentsDirectory {
return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
}
And here's an example of how I'm building a path to an image
path = [NSString stringWithFormat:#"%#/%d/%#", [self applicationDocumentsDirectory], [[thisItem valueForKey:#"publicationID"] intValue], [thisItem valueForKey:#"coverImage"]];
It turned out to be an issue where the bundle was not being updated on the device and apparently didn't have the same set of files that the Simulator had. This blog post helped a bit: http://majicjungle.com/blog/?p=123
Basically, I cleaned the build and delete the app and installed directly to the device first instead of the simulator. Interesting stuff.
I don't see where documentsDirectory is defined.
NSString *destinationPath = [documentsDirectory stringByAppendingPathComponent:[NSString stringWithFormat:#"%d", [[[manifest objectAtIndex:i] valueForKey:#"publicationID"] intValue]]];
Perhaps the following will do the trick
NSString *destinationPath = [[self applicationDocumentsDirectory] stringByAppendingPathComponent:[NSString stringWithFormat:#"%d", [[[manifest objectAtIndex:i] valueForKey:#"publicationID"] intValue]]];
Your copy code looks fine, and you say you're getting no errors.
But I'm intrigued by "The assets just come up as null." Are you sure you're accessing the file name later with the exact same name?
Unlike the simulator, a real iPhone has a case sensitive file system.
I am developing an iPhone app with someone else. The app works fine for me, but he is running into a bug. We think this bug is related to the fact that he is getting multiple Application directories for this same app. In my ~/Library/Application Support/iPhone Simulator/User/Applications, I only have one folder at all times.
He says that he will get 3 or 4 directories when he is only working on this one app. We think this is our problem because our bug has to do with displaying images that are stored in the app's Documents folder. Does anyone know why he is ending up with multiple directories or how to stop it?
Edit:
Here is the code for writing the image to a file:
NSData *image = [NSData dataWithContentsOfURL:[NSURL URLWithString:[currentArticle articleImage]]];
NSArray *array = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *imagePath = [array objectAtIndex:0];
NSFileManager *NSFM = [NSFileManager defaultManager];
BOOL isDir = YES;
if(![NSFM fileExistsAtPath:imagePath isDirectory:&isDir])
if(![NSFM createDirectoryAtPath:imagePath attributes:nil])
NSLog(#"error");
imagePath = [imagePath stringByAppendingFormat:#"/images"];
if(![NSFM fileExistsAtPath:imagePath isDirectory:&isDir])
if(![NSFM createDirectoryAtPath:imagePath attributes:nil])
NSLog(#"error");
imagePath = [imagePath stringByAppendingFormat:#"/%#.jpg", [currentArticle uniqueID]];
[image writeToFile:imagePath atomically:NO];
And here is the code for getting the path when I need the image:
- (NSString *)imagePath
{
NSArray *array = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *imagePath = [array objectAtIndex:0];
return [imagePath stringByAppendingFormat:#"/images/%#.jpg", [self uniqueID]];
}
The app works great for me, but my partner says that the images don't show up intermittently, and he notices that he gets multiple directories in his Applications folder.
I had this problem (I was saving photos in the apps documents directory) and after every new build the directory get's renamed, so my paths were no longer valid. I cooked up these 2 functions (in my app delegate) that will give me a path for the file I want to save or load from the documents or temp directory. Even if the app directory changes, as long as you only store the file name and not the full path, and then use your helper functions to get the path when you need it later you will be ok. Here's my functions for this:
+ (NSString*)fullPathToFile:(NSString*)file {
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString* documentsDirectory = [paths objectAtIndex:0];
return [documentsDirectory stringByAppendingPathComponent:file];
}
+ (NSString*)fullPathToTemporaryFile:(NSString*)file {
return [NSTemporaryDirectory() stringByAppendingPathComponent:file];
}
Works like a charm.
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];