How necessary is it to search for a path to an image using the NSBundle method pathForResource when creating a UIImage using imageNamed? I see tutorial codes that just specifies the name of the image directly, and then code that goes the extra mile to find the path first.
In my experience, I've always just used the name directly and it's always worked fine. I assumed that it automatically knew how to find the image. How important or under what circumstances would it be necessary to do more than this?
Not at all ... is the answer to the original question:
How necessary is it to search for a path to an image using the NSBundle method pathForResource when creating a UIImage using imageNamed?
Not much .. is how correct the accepted answer from Zoul and the other one from Ranga are. To be fair: they are correct if you're talking about the application bundle directory structure, or for the (rare) case where the image is in a "blue" folder in Xcode (more on that later), but not for the most common cases
Anyway, on to the one true answer.
As usual I found this question while trying to find the answer myself. I never found the documentation or the other answers to this question satisfactory, so I decided to test.
My test details are all below, but let me summarize the results here.
In short, when using imageNamed: to load your images, it depends where you put them:
if your images are in the root of your project, even if organized in a purely logical Xcode group, then no, you don't
need to think about the path: just the image name.
if your images are in a group that is attached to a directory in your file system, via "create groups for added folders" then you still don't need to worry about the name.
if your images are in a "blue" group, that is attached to a directory in your file system via "create folder references for added folders", then you can load it with imageNamed: by specifying a relative path as suggested (coincidentally?) by the accepted answer above.
if you use the main alternative to imageNamed:, imageWithContentsOfFile:, you do indeed need the full path to the file including the bundle path, which means you need to know how the Xcode navigator structure translates into paths in your bundle directory structure.
The other important differences between these two methods:
imageNamed doesn't require that you specify the filetype extension,
so merely "icon" not "icon.png", whereas imageWithContentsOfFile does
require the full file name
this first point helps with the second feature: imageNamed will
automatically load the retina version of an image if there is one, by adding #2x to your file name. So if you ask for "icon", on a
retina display it will try to load "icon#2x.png".
imageWithContentsOfFile does not
imageNamed caches the image: and therein lies a lot of the
controversy around it: If you search SO or the web at large you'll
find a lot of posts recommending you avoid it because it doesn't
clear its cache properly. This, however, was fixed years ago, so you
don't need to worry about it failing to clear its cache. You do still
need to worry about the fact that it caches at all, though. If your
images are big and are not loaded very often, you'll conserve
memory by loading them from file and not caching them. This is
nothing to do with leaks: even if you don't have leaks, you still
have limited memory on the device, and you don't want to cache
unnecessarily. It's the classic caching tradeoff: what's more
important in your situation? Memory performance or cpu performance
(time).
So on with my tests.
What I did was create a simple UITableView app with 3 simple icon files, shown in the rows of the table using different methods. The icons are different in their location in the Xcode project structure. Note the emphasis on Xcode. The key to understanding the answer to the original question is that there are three totally different project directory structures in an iOS app: There's the one you see in the Xcode navigator, the one on the file system for the same project that you see in Finder (right click any item in the Xcode navigator and select "show in Finder") and, the one you seldom see, the "bundle" directory structure of the deployed app. You can see this last one in Finder too - by finding your app in ~/Library/Application Support/iPhone Simulator, and drilling down into the .app directory. I'll show you a picture of mine in a minute.
So in my app, I dragged all three icon png image files into Xcode in different ways:
icon1.png (a clock), I dragged in as a file to the root of the Xcode project,
then I later created a new group in Xcode and dragged it into
that. This group is not represented by any directory in the file
system: it's a pure Xcode group. Hence it's name: "JustGroup"
icon2.png (an eye), I originally put my file system in directory called
"RealDir", and I dragged this entire directory into Xcode, and when
asked, I chose the "Create groups for any added folders" option.
This means that the RealDir group in Xcode is attached to a real
directory called RealDir in the filesystem (in my project directory)
and that icon2.png is in there.
icon3.png (a target), I also had in a separate directory, which I also dragged
into Xcode. Only this time I chose the 2nd radio option "Create
folder references for any added folders". This creates a so-called
"blue" group in Xcode. More on what this is all about later. I
called this group (and directory) "FolderReference"
Here's a shot of the choice that Xcode gives you:
And here's what my project structure looks like in Xcode:
Now, in my app, I used two methods for loading each of the icons: UIImage imageNamed: and UIImage imageWithContentsOfFile. I created a bunch of rows in my table with the title of the each cell being the name of the group containing the icon: JustGroup, RealDir or FolderReference, plus the name of the method used: imageNamed vs fromFile (which I'm using as abbreviation of imageWithContentsOfFile)
The detail label of the cell (the fainter text under the title) shows the file or path name I gave to the method.
To be clear, in the case of "fromFile", I'm adding the bundle path to the "relative" name you see.
So for "fromFile", I'm actually using this code:
NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
NSString *imagePath = [NSString stringWithFormat:#"%#/%#", bundlePath, filePath];
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
where "filePath" is the path you see in the table cell detail label.
For imageNamed:, on the other hand, the filePath in the cell detail is passed verbatim.
And the row image is, naturally, the image that is loaded. So for the rows in the table that have no image, the image loading failed.
Here, in a nutshell, are the results. If you read nothing of this post, at least a glance at this image will tell you all you need to know.
Here's the basic explanation in easily digestible points:
as it states in the official documentation, the imageNamed: method loads images from the application bundle. This means you don't need to specify the bundle location, only the file name. And even then, just the base name of the file. The documentation is a little thin here, it should really make it clear that it loads the image from the given file path relative to the application bundle root directory.
(here's the kicker, pay attention to this one) that rule about the bundle directory, refers the root directory in your the bundle of your deployed app. If you go exploring, that means in the ".app" directory itself. That's not the same as the root directory of the Xcode project in the Xcode navigator, nor is it the same as the root directory of the Xcode project in finder
this is because, when deploying your app to the device (or simulator) all project directories represented by "groups for added folders" are flattened. That is, the directory is ignored, and all of its contents dumped unceremoniously into the root directory of the bundle. (I say "unceremoniously" because if there are files with the same name in different folders, they'll collide here and you'll get no help in resolving the problems that causes.) This is the case of RealDir in my example: in the deployed app, RealDir no longer exists, and icon2.png is left to mix in with the general population (scary). It goes almost without saying that "JustGroup", the purely logical Xcode group, is also ignored - it was never a real directory anyway, just a visual aid to the Xcode user - and icon1.png is also in the bundle root.
This is why imageNamed: was able to load icon2.
And also why imageWithContentsOfFile was not able to find it in "RealDir/image2.png": because there is no RealDir directory in the deployed app.
"blue folders", on the other hand, that is, directories represented by "folder references for added folders", are in fact retained in the app bundle directory structure. This, apparently is the point of blue folders: they give you a way to create a directory structure in your deployed app. I'm not sure of the original raison d'etre for this, but one good use case is where you have several directories containing alternative versions of resource files with the same name and you want your app to be able to switch between them at runtime by changing directory. Anyway, the icon3.png in my FolderReference, remained in my FolderReference directory in the deployed app.
This is why imageNamed: couldn't find it with "icon3", but could find it with "FolderReference/icon3"
imageWithContentsOfFile was able to find it also using FolderReference, but only when attached, remember to the full bundle path using the code above. (Key difference here: imageNamed works with a relative path in this case, imageWithContentsOfFile always works with an absolute path).
To clarify, here are my folder structures:
You saw my Xcode project navigator structure above, here is the file system directory underneath it:
And finally, perhaps most importantly, the deployed bundle file system directory structure:
Note: I found this at this location on my Mac: you will find yours in a similar location - you might have to search a bit to find which ugly-GUID-named subdirectory contains your app.
~/Library/Application Support/iPhone Simulator/5.1/Applications/4EB386B2-CD7E-4590-9757-18DDDEE6AF4F/ImageLoadingTest.app
I hope this helps. Testing, exploring and finally, describing it certainly helped me.
The docs say that “the method looks for an image with the specified name in the application’s main bundle”, so I’d say you can always use just the name. The only exception might be images stored inside subfolders, especially when you have foo/image.png and bar/image.png. I don’t know whether [UIImage imageNamed:#"foo/image"] would work, but it’s trivial to try.
(What’s a bit confusing in these cases is that the groups in the Xcode tree do not correspond to folders in the resulting app bundle. Their contents are smashed together to the root of the bundle, unless you use a blue folder reference instead of a regular group.)
I created a new Xcode project (single view, AppDelelgate, ViewController class, storyboard etc ).
Created an Images group.
Used Paintbrush to create a 16x16 png file Wall1.png and dropped it into the Images group in Xcode (let Xcode copy the files).
In the ViewController viewDidLoad method added the code:
UIImageView* imageView=[[UIImageView alloc] initWithFrame:CGRectMake(50, 100, 16, 16)];
UIImage *image = [UIImage imageWithContentsOfFile: [[NSBundle mainBundle] pathForResource:#"Images/Wall1" ofType:#"png"]];
imageView.image = image;
UIImageView* imageView=[[UIImageView alloc] initWithFrame:CGRectMake(50, 100, 16, 16)];
UIImage *image = [UIImage imageWithContentsOfFile: [[NSBundle mainBundle] pathForResource:#"Wall1" ofType:#"png"]];
imageView.image = image;
[self.view addSubview:imageView];
Ran the app on my phone, the image would not appear
Added a break point at [self.view addSubview:imageView];
image was null
Opened terminal and changed directory to my project, Wall1.png was not a group folder Images. Removed the png from the project, created a Images folder, moved Wall1.png into the folder. Added the existing file Wall1.png to the group Images.
Ran the app, the image still doesn't appear.
image was null
Changed Images/Wall1 to Wall1
Ran the app, boom image is displayed1
If you create a group for your images, Xcode does not create a respective directory. Create one manually if you want to (I prefer to keep my images in a seperate folder). Dont specify the full path to your image file when using UIImage imageWithContentsOfFile.
Try this.
[UIImage imageNamed:#"your directory path of image"]
[UIImage imageNamed:#"Dir1/folder1/folder2/imagename.jpeg"]
I have a localized iOS application in which I wish to include some localized HTML files. I can't work out how to do it.
Currently, my folder structure looks like this:
/myapp
/en.lrproj
/Localizable.strings
/fr.lrproj
/Localizable.strings
/webviews
/view1
/index.html
/pic1.png
/view2
/index.html
/pic2.png
As you can see, I currently have the views organized into their own folders with the associated images.
In XCode, when I selected the Localizable.strings files I can add new localizations. When searching for the solution to this problem I see that other people have done the same for the HTML files, however when I select the HTML files there are no options displayed for localizations, so I'm wondering if the folder structure is the problem.
On the other hand, I don't know how to structure the HTML into language code folders whilst not replicating the graphics that have to be along side.
Clearly I'm not understanding something - what do I need to do to get this working?
Thanks,
Tim
Arrange your index.html right next to the Localized.strings, keep the pics in the web views directory:
/myapp
/en.lrproj
/Localizable.strings
/view1/index.html
/view2/index.html
/fr.lrproj
/Localizable.strings
/view1/index.html
/view2/index.html
/webviews
/view1
/pic1.png
/view2
/index.html
/pic2.png
The build a file path:
NSArray* availableLocalizations = [[NSBundle mainBundle] localizations];
NSArray* userPrefered = [NSBundle preferredLocalizationsFromArray:availableLocalizations forPreferences:[NSLocale preferredLanguages]];
NSString *filePath = [[NSBundle mainBundle] pathForResource:#"index" ofType:#"html" inDirectory:#"view" forLocalization:[userPrefered objectAtIndex:0]];
The picture in the html tree are now similar to
/myapp/en.lrproj/view1/index.html
/myapp/webviews/view1/pic1.png
Inside your index.html make the <img> tags point to ../../../webview/pic1.png
(Not sure about the number of ../'s that you need. You might want to open a terminal, navigate to /myapp/en.lrproj/view1/ and test with ls ../../../webview/pic1.png. )
I assume "webviews/view1/index.html" is English, and "webviews/view2/index.html" is French.
NSString *dir = NSLocalizedString("webviews/view1", #"webdir");
NSString *path = [[NSBundle mainBundle]pathForResource:#"index" ofType:#"html" inDirectory:dir];
in Localizable.strings of French
/* webdir */
"webviews/view1" = "webviews/view2";
I tested above code using below in a ViewController.
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:path]]];
"webviews" directory should be a "folder reference" in Xcode, not a group.
(Blue directory icon, not yellow one)
Also, be careful not to add a slash after the directory name, like "webviews/view2/".
I found that this could be a problem when I run that on iOS 5.x(I tested it on 5.0.1 and 5.1.1). But no problem on iOS 6.1.2.
I have an app that uses a Table View to show a list of stories a user can read and when they tap on a particular title a Detail View will open up and the story is displayed.
To begin with I had the app loading up the stories directly from the web and this worked perfectly. I used an array and to pass the details of the particular stories web address and used the following to load up the page
[detailWebView loadRequest:[NSURLRequest requestWithURL:detailURL]];
Now I want to load up files locally instead and from searching around I found the following
[detailWebView loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:detailURL ofType:#"html"]isDirectory:NO]]];
and it does load up my local 'Story 1' HTML file but that same HTML file gets loaded up regardless of the file name being passed by detailURL in my Table View which takes the format of
[bookOne addObject:[[NSMutableDictionary alloc]
initWithObjectsAndKeys:#"Story One Title",#"name",
#"Story 1",#"url",nil]];
If I pass it a file name that doesn't exit the program quits so I'm pretty sure the different file names are being passed but the same HTML page always shows.
I've tried reboots etc but the same file always opens up, your help would be really appreciated.
Thanks
Kieron
Try to load the files with
loadHTMLString:baseURL:
it might be that NSURLRequest caches the file/response.
A different solution would to create an instance of the NSURLRequest with initWithURL:cachePolicy:timeoutInterval: and pass NSURLRequestReloadIgnoringLocalAndRemoteCacheData for the cachepolicy
Thanks Nick, while loadHTMLString didn't provide the answer it got me thinking differently and on the right track as to the real cause of the problem.
When using a web address in the first example detailURL had been set as NSURL and when I changed it to NSString my code worked fine. I don't understand why it would cause the loading of the HTML file(s) to act so weirdly but everything is running fine now, the different stories all load up as there should and there are no caching issues of any sort.
I'm writing an iPhone app with a UIWebView which should display various html files I have in the app resource folder. In xcode my project overview, these html files are displayed like this:
dirA
|---> index.html
|---> a1.html
|---> a2.html
|---> my.css
|---> dirB
|---> b1.html
|---> b2.html
|---> dirC
|---> c1.html
|---> c2.html
These resources where added to the project as such:
Checked "Copy items into destination groups folder (if needed)".
Reference type: Default.
Text encoding: Unicode (utf-8).
Recursively create groups for any added folders.
The links in my html are relative, meaning they look like this:
a1
a2
b2
c1
In order to display the index.html when the app starts up, I use the following code:
NSString *path = [[NSBundle mainBundle] pathForResource:#"index" ofType:#"html"];
NSURL *url = [NSURL fileURLWithPath:path];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[webView loadRequest:request];
This works fine. Following links from the index file also works fine, as long as the html files requested are directly under dirA. If the link followed points to a file in a sub-directory, then didFailLoadWithError will catch the situation and report that the requested file does not exist.
Also,
[webView loadHtmlString:myHtml];
cannot be part of the solution, as I need back and forward buttons to work in my web view.
So the question is: How can I follow a relative link to an html file in a sub directory within my resources?
I've been all over stackoverflow and the rest of the tubes for the past few days trying to figure this one out, but nowhere have I come across the solution to this exact problem. Any insight at all would be very, very much appreciated!
EDIT: Yoohoo! I figured it out! What joy! Here is what I did:
Imported my resources anew, choosing "Create folder references for any added folders" instead of "Recursively create groups for any added folders."
Specified the root directory for resource, like so: NSString *path = [[NSBundle mainBundle] pathForResource:#"index" ofType:#"html" inDirectory:#"dirA"];
To create folders in the app bundle drag the folder to Xcode and select the radio button: "Create Folder References for any added folders".
The subdirectories you use in XCode are groups not actual folders. all of the resources are likely being flattened out to the output folder. If you create actual folders outside of XCode that might work. Try creating the folder and file heirarchy and drag/dropping into XCode. Also check your build folder using Finder to see exactly how XCode is deploying your files.
I managed to use HTML files containing resources and links using relative paths by using the +fileURLWithPath: initializer instead of +URLWithString: when loading an HTML string and passing a baseURL in the web view.
NSString* path = [[NSBundle mainBundle] pathForResource:#"index" ofType:#"html" inDirectory:#"HTMLContent"];
NSString* htmlString = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error];
[self.webView loadHTMLString:htmlString baseURL:[NSURL fileURLWithPath:path]];
"HTMLContent" is my equivalent for your "dirA". No other changes were required.
I was using NYTimes iPhone application, I become bit curious when I notice that it cache UIwebview even though I didn't open that article.Does anyone have idea how to do that?
How NYTimes iPhone application doing offline reading?
Any help greatly appreciated.
Thanks,
Amit
I wrote the NYTimes app. Here are some details that you could have gotten by looking inside the app bundle.
I download the HTML for the articles, strip out whatever unsupported HTML and JS crud the producers stuffed in it and cache it in the backing store.
The article content is contained in a series of P tags (HTML fragment). I stuff that into a special HTML skeleton page that ships with the app. The static wrapper page also contains CSS and JS used to properly display the article and lazily load the images.
The image loading is really cool. The web view is notified when the images are ready. layout is not affected because I already know the sizes of the missing images.
You can use UIWebView to load html files stored locally on the iPhone.
NYTimes app is probably caching html and images in local storage.
Search google for "UIWebView local" and you get several useful hits.
I tried it out and it works great:
First, create a "view based application" and add a UIWebView to the NIB.
Second, add this code to your UIViewController code:
- (void)viewDidLoad
{
NSString *path = [[NSBundle mainBundle] pathForResource:#"index" ofType:#"html"];
NSURL *url = [NSURL fileURLWithPath:path isDirectory:NO];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[webView loadRequest:request];
[super viewDidLoad];
}
Third, add a file called "index.html" to your "Resources" folder in xcode and it will be displayed.
UPDATE:
Indeed, the complicated part of this is downloading the images and stylesheets for the webpage. Doing this server side is easy with Simple HTML Parser (and PHP). Just package everything in a zip and download to your iPhone.
Alternatively, you could do it locally with a C/C++/OBJC HTML parser (libxml2.2 is available on iOS). See this SO question Parsing HTML on the iPhone.
It's going to a bit of a project, so good luck.