I have about 60 images I want to store in Core Data, 30 of which are avatars and have the prefix of avt_filename_00X.png and 30 of them are smaller and have a different prefix.
Rather than storing all the images as BLOBs in Core Data/SQLite, I want to store the paths for each image found (in the same way you would store image paths for a MySQL database).
However I am not sure how to grab the path of the image as found in NSBundle.
I can get the path to the NSDocumentDirectory via:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSFileManager *fileManager = [NSFileManager defaultManager];
[fileManager fileExistsAtPath:documentsDirectory];
NSLog(#"documentsDirectory = %#", documentsDirectory);
And I can load the image and add it to an array.
if (qty == 0)
{
//NSLog(#"fileToLoad = %#", fileToLoad);
UIImage *img = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:fileToLoad ofType:fileExt]];
[self.avtList addObject:img];
[img release];
} else {
// load multiple image into an array
// not coded yet
}
But, what I'm unsure of is:
How do I grab the path where the computer found the image once its inside the NSBundle?
How can I be sure that the path will work when the app is on a device?
The idea would be to get all the images stored in an array and then push them to Core Data/SQLite at a later time.
The correct way to get the full path to a resource in the main bundle is:
[[NSBundle mainBundle] pathForResource:#"avt_filename_00X" ofType:#"png"]
(or you can supply the empty string for 'ofType' if you prefer to include the extension in the resource name)
But nowhere in the docs is the path guaranteed to remain the same across devices, operating system iterations, etc. It's the path to that file from the application bundle in the current environment, guaranteed to remain valid for the duration of this run of the application only.
Because the path to the application, and hence to its resources, isn't guaranteed to stay the same, I think it's explicitly unsafe to put it in an SQL database by any means.
Could you perhaps adopt a scheme whereby a filename starting in / is a complete path, one without a / at the start is assumed to be in the bundle, meaning that you can apply the logic on the outside of the database?
How can I be sure that the path will
work when the app is on a device?
Therein lies the rub: you can't. You would be best to let the paths be handled on-the-fly, and perhaps just store the file names instead.
Related
I'm archiving an object to a file - on simulator it works perfectly, but on a tvOS (Apple TV) device it doesn't (in other words -(BOOL)archiveRootObject:toFile: returns a funny NO):
static NSString * _DocumentsDirectory() {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentsDirectory, NSUserDomainMask, YES);
return paths[0];
}
and:
NSString *fullPath = [_DocumentsDirectory() stringByAppendingPathComponent:filename];
[NSKeyedArchiver archiveRootObject:obj toFile:fullPath];
where filename = #"foo.data" and obj conforms to NSCoding.
I tried different ways to put together the path and different directories, but the result is the same.
The only thought I have is that there could be some writing permission I need to set.
Any idea?
After investigating a bit, I found out that the only ways that Apple allows you to store data in tvOS apps are the following:
Therefore, I opted for using the Cache directory (which is purged only if space is needed) for data that I can eventually re-download, and NSUserDefaults for smaller data which I need to always access locally.
Does the following method ever return multiple values when used on iOS, and if so, do you have an example of when this happens and how to know which element is the one you asked for?
NSArray* NSSearchPathForDirectoriesInDomains (NSSearchPathDirectory directory, NSSearchPathDomainMask domainMask, BOOL expandTilde );
I am wondering because I am calling it with the parameters for getting a path to the Documents directory (see below), and assuming it returns an array with only one element. It is working just fine, but it occurred to me that I might need to make sure I will never get more than one element back. And if I do get more than one, I wondered how I would know which one is the one I asked for?
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *docPath = paths[0];
It's my understanding that on iOS you'll only ever get a single element in the returned array.
I believe it returns an array rather than a single value as it's used by OS X too and tries to be as general purpose as possible. On OS X it can potentially return multiple elements, representing the user's documents directory, the computer's documents directory, and so on, according to the domain mask.
As an aside, note that Apple recommends using the NSFileManager methods URLsForDirectory:inDomains: and URLForDirectory:inDomain:appropriateForURL:create:error: instead, stating that URLs are preferred over path strings.
Nothing is written in my plist file after this code. What is wrong with my code?
NSString *path = [[NSBundle mainBundle] bundlePath];
NSString *finalPath = [path stringByAppendingPathComponent:#"aFile.plist"];
NSMutableDictionary *reqData = [NSMutableDictionary dictionaryWithContentsOfFile:finalPath];
/*
some modifications to "reqData"
*/
[reqData writeToFile:finalPath atomically:YES];
Nothing is written in file. what could be the problem?
You'd better write to Document folder:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask,
YES);
NSString *path = [paths objectAtIndex:0];
Update
According to NSDictionary Class Reference:
This method recursively validates that all the contained objects are property list objects (instances of NSData, NSDate, NSNumber, NSString, NSArray, or NSDictionary) 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.
Is there any objects with a type other than these valid ones in your whole dictionary?
You are trying to write the file back to the app bundle.
That's what's most likely causing the error.
What if you try writing it somewhere else (e.g. in your desktop folder)?
If you're including the plist with your application, you'll want to copy that file into the Documents directly when the app first starts up (if it hasn't already been copied there). Then, any read and write operations you want to do on the plist should be done from the copy in the Documents directory instead of the version in the app bundle.
First Thing to remember:
You can read a plist file from resources but you can't modify it.
if you want to modify,
copy that file to Documents directory
copy contents of plist into array or dictionary depending on its type
make changes you want
Finally.... save it back to documents directory
that do the trick
I'm getting strange behavior writing NSString and NSData objects to relative file paths. Here's an example:
NSString *string = #"I am a file!";
NSError *error = nil;
NSString *fileName = #"text.txt";
BOOL written = [string writeToFile:fileName atomically:YES encoding:NSUTF8StringEncoding error:&error];
if (written) {
NSLog(#"Successfully written to file.");
} else {
NSLog(#"Error: %#", [error localizedDescription]);
}
When I run this I always get "Successfully written to file.", but the file is never there. Somehow the program thinks it was successful and no error is generated.
What am I doing wrong? (I'm on Mac OS X Lion)
This writes to the current directory. The default current directory when you run something under Xcode 4 is going to be ~/Library/Developer/Xcode/DerivedData/<prodDir>/Build/Products/<configuration>. You can override this using a Scheme. When you run a program from the commandline, then the current directory is whatever the current directory was when you ran the program. If you use Finder to launch the app, then the current directory will often be /.
In principle, it's fine to write the current working directory. It's very common to do this in command-line apps. So regarding #craig's comment about writeToFile: expecting an absolute path, I don't think that's really true. It expects and writes to a path. It doesn't care if it's absolute or relative. This is a Foundation class, and is just as useful in a command-line program as a GUI.
But in a GUI app, you should avoid relative paths. In principle, you could set the current directory and then write the file, but this is usually a bad idea in a large program since it's not thread safe (there is only one cwd for the whole program). And GUI apps tend to have somewhat unpredictable current directories, so it doesn't make for a good user experience.
But to the question of why you didn't get an error, it's because it probably successfully wrote it. You just didn't know where to look.
NSFileManager * fm = [NSFileManager new];
NSString * dirPath = [fm currentDirectoryPath];
NSString * absPath = [dirPath stringByAppendingPathComponent:#"myfile.file"];
[fm release];
keep in mind that currentDirectoryPath reflects your programs working directory until you change it with -changeCurrentDirectoryPath:, the programs working directory can be different depending on how it was launched, and can't be relied upon.
The first parameter to the writeToFile: method (in your example) is a relative path, but you probably want to use an absolute path. Otherwise, the system will place your files relative to the current executable. When you're running inside Xcode, this might not be where you expect them to end up. (As Rob mentioned in his answer, this location is somewhat buried, and can change depending on which version of Xcode you're using.)
If you want to build up a directory path using NSString objects, I would recommend the stringByAppendingPathComponent: method:
...
NSString *directory = #"/Users/Mikael/Desktop";
NSString *filename = #"MyFile.txt";
NSString *fullPath = [directory stringByAppendingPathComponent:filename];
...
Note that this method will take care of making sure your slashes are well-formed.
i was wondering why we search a path with NSDocumentDirectory at first here :
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask,YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *writableDBPath = [documentsDirectory stringByAppendingPathComponent:#"Sports.sqlite"];
and later on we compare this path with another path, using this time resourcePath from the NSBundle :
NSString *defaultDBPath = [[[NSBundle mainBundle] resourcePath]
stringByAppendingPathComponent:#"Sports.sqlite"];
success = [fileManager copyItemAtPath:defaultDBPath
toPath:writableDBPath error:&error];
What is the difference between both?
In this situation you are (normally one time only, unless you need to restore database) copying the database from your read only bundle into your documents directory so that a user can read/write to it. This is useful if you want to pre-seed a database or just have the structure set up.
Your documents directory is read/write and your bundle is not therefore you need to have the sqlite in your documents directory for it to be used properly.
The first part of code is simply getting you the path for where you want the sqlite file to live in your documents directory. Which ends up being held in writableDBPath.
Next you get the path from your bundle (defaultDBPath) and use the two paths to
... copyItemAtPath:defaultDBPath toPath:writableDBPath ...
This gives you a read/write database that you provide in your bundle. Why would you do this instead of running your SQL on the device to create the schema? This allows you to pre-seed the database with some data. It can sometimes be easier to use a graphical tool to set up and edit your sqlite file
What you mean by "comparing" the two paths?
What I see is a file copy from your app resources directory to the user document directory.
So what is happening is that a default version of Sports.sqlite that is to be bundled with the app (and thus is available in the resources directory) is copied to the user directory where the user can modify it.
The first chunk of code simply build the destination path (a string); the second chunk build the source path (a string) and then makes the copy.
Does it make sense?