I'm using the UIDocumentInteractionController with a temporary file that resides in my cache after having downloaded it. I'm using a quite simple class that delivers md5-cache file names (ext = cache) to my app and the downloaded file is in this format. The reason is to have files locally and to only download them once (a session). Since the cache names are in a uniform format I can easily clean them up.
Now with UIDocumentInteractionController I need to rename these files back to their original name or they will not get recognized correctly.
When the UIDocumentInteractionController finishes handing off the file I thought to move the file back to its cache file name. The problem is, the method: - documentInteractionController:didEndSendingToApplication: never gets called - though the delegate is set correctly.
How I basically set up the controller:
interactionController = [UIDocumentInteractionController interactionControllerWithURL:[NSURL fileURLWithPath:temporaryFile]];
interactionController.delegate = self;
interactionController.annotation = [cacheURLString lastPathComponent]; // original name to move back to
Any suggestions on how to correctly find out that a file has been handed over to another application / or the UIDocumentInteractionController has been dismissed?
I found that documentInteractionController:didEndSendingToApplication: is called if the document is sent to another application, but isn't called when sending the document via email (and possibly other built in functions like AirDrop, copy, print, etc.). This seems like a bug to me, but there it is.
Tony's answer in the comments worked for me - use [[NSFileManager defaultManager] linkItemAtURL:toURL:error] to link the source file to a temporary file and pass the temporary file to the controller. This isn't taking up a lot of additional space and the temporary link will be removed after a certain amount of time.
Related
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 an encrypted word/excel/pdf file locally stored which I need to preview in my iPad app. I understand that QLPreviewController or UiDocumentInteractionController could be used to preview these files. I can very well use this
- (id <QLPreviewItem>) previewController: (QLPreviewController *) controller previewItemAtIndex: (NSInteger) index {
return [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:[documents objectAtIndex:index] ofType:nil]];
}
But the file is encrypted and when I decrypt it I would get hold of NSData object. How do I go about loading NSData in either of these.
Also I understand that I can very well store the NSData back as a local file and load it in Preview. But there is a constraint of not storing the unencrypted file locally.
If someone has already accomplished this and can help me out here it will be greatly appreciated.
Thanks
AJ
Since you are using Quick Look, your options are limited. You must give Quick Look an NSURL, which means it must be on the file system (or the Internet). Fortunately, this shouldn't be much of a problem. iOS devices use hardware-level encryption. When your file is encrypted, only your app has the key to decrypt it. So, your file will still be encrypted, but it will also be readable by your app and only your app.
Here's what you do:
Decrypt your file into an NSData object, which you've already done.
Write the file to a location that will not get uploaded to iCloud nor backed up by iTunes. The tmp directory is probably the best choice. The code looks something like this:
NSData * data = // Your decrypted file data.
NSString * fileName = // Whatever you want to name your file.
NSString * path = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
NSURL * url = [NSURL URLWithString:path];
NSError * error = nil;
BOOL success = [data writeToURL:url
options:NSDataWritingFileProtectionComplete
error:&error];
if (success) {
// Give the URL to Quick Look.
}
else {
// An error happened. See the 'error' object for the details.
}
At this point you have an NSURL which you can use with Quick Look. Don't forget to delete the decrypted file when you are done with it.
There are a few things to note about on-disk encryption:
It is only supported on iOS 4.0+.
It may not work on "older" devices.
The user must have an active passcode.
If you use NSDataWritingFileProtectionComplete, the file is not accessible while the device is locked. If you need to access the file while the app is locked, then you should use NSDataWritingFileProtectionCompleteUnlessOpen or NSFileProtectionCompleteUntilFirstUserAuthentication instead. This will still give you great protection, even if the device is stolen and jailbroken. Be aware, though, that these encryption options are only available on iOS 5.0+
For more details for on-disk encryption, check out the iOS App Programming Guide
After doing some digging, I found out that QLPreviewController is using UIWebView underneath, and calls the loadRequest: to load the requested file.
Another way to accomplish what you desire is to make a private Category on UIWebView,
and use method swizzling to override the loadRequest: method, and call instead the loadData:MIMEType:textEncodingName:baseURL: method.
Beware that:
1) In low-memory scenarios (i.e. large files) a black screen with
"Error to load the document" appears, if that concerns you. (The
unhacked QLPreviewController knows how to handle these scenarios
very well and present the document).
2) I'm not sure Apple are going
to approve this kind of hack, although no private APIs are used
here.
code:
#implementation UIWebView (QLHack)
- (void)MyloadRequest:(NSURLRequest *)request
{
// Check somehow that it's the call of your QLPreviewController
// If not, just call the original method.
if (!insideQLPreviewController)
{
// Call original implementation
[self MyloadRequest:request];
}
else
{
// Load the real data you want
[self loadData:data MIMEType:mimeType textEncodingName:nil baseURL:someURL];
}
}
+ (void)load
{
method_exchangeImplementations(class_getInstanceMethod(self, #selector(loadRequest:)), class_getInstanceMethod(self, #selector(MyloadRequest:)));
}
#end
Actually, writing a file to a tmp directory is still insecure. The other alternative is to use UIWebView with NSURLProtocol and allow decrypting this data on the fly.
One way could be.
use Temp Dir , Save File in Temp , Make NSURL From that Temp File and Display and then Delete that temp Dir after that.
Thanks.
I am developing an App that displays several pages of Text when the correct buttons are pressed. It is Static proprietary information. There is a different Text File for each of six buttons.
I am new to the ios SDK. Does creating a project in XCODE automatically create a Documents Folder? Is the "Documents Folder" what Apple is calling the "Sandbox"?
Can I simply write my Text, (that part which will display on the screen, LOTS of Text), drop it into the "Documents Folder", then display it in "scrolling mode" on the iPhone when a certain button is pressed?
I would prefer the Text to be part of the compile, since the information is proprietary, not simply a Text File, if there is a way to store and display large Text Files efficiently.
Yes Ken, the documents directory is there by default when you create an app, and yes, you can certainly write and read text data from it if you want to.
You cannot directly drop data into the Documents folder however, you need to do so programmatically.
Assume that one of your files are 'TextFile1.txt'. You should add this file to your project firstly, and somewhere in the appDelegate, write the following code;
NSString *fileBundlePath = [[NSBundle mainBundle] pathForResource:#"TextFile1" ofType:#"txt"];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *fileDocumentDirectorySavePath = [documentsDirectory stringByAppendingPathComponent:#"TextFile1.txt"];
NSFileManager *fm = [NSFileManager defaultManager];
if (![fm fileExistsAtPath:fileDocumentDirectorySavePath])
[fm copyItemAtPath:fileBundlePath toPath:fileDocumentDirectorySavePath error:nil];
This will copy the TextFile1.txt into your apps Documents folder from which you can read it anytime you need with the following code;
// You can get the fileDocumentDirectorySavePath same way as in the above code
NSString *stringToDisplay = [[NSString alloc] initWithData:[NSData dataWithContentsOfFile:fileDocumentDirectorySavePath] encoding:NSUTF8StringEncoding];
NSLog(#"String : %#", stringToDisplay);
You can do this for any number of text files you need to work with.
If you don't want to change the text dynamically (i.e., only when you submit an update), just add the files directly to your Xcode project and don't even worry about the sandbox/Documents folder. You can drag the files into the sidebar in Xcode (creating a custom folder for it would be very organized of you) and check the "Copy files to project folder?" when asked. As is stated, they're now copied and part of the compiled app. Then you can query the files and display them in a UITextView, automatically supporting scrolling of text.
Alternatively, you could do what I think is the easier method and include the files directly in your code. In a class file that loads the text, in the .h file (Header), add a UITextView as a property and a variable. In the .m file (Implementation), do yourTextView = [[UITextView alloc] init];, then set yourTextView.text to an NSString containing your text. Sounds confusing, but it will be quicker and easier to update in the end. That is, unless your text is formatted... Anyway, you could also just create a UITextView in your XIB/NIB file and add your text directly.
I'd suggest you do it in code. That will be the easiest to change.
Adding text to your app is one thing - making it secure is more difficult. I had to deal with a similar issue and decided to use unformatted text that I include encrypted in my app and only decrpt the part that is being shown. Really depends how "secret" you want to keep the text. Remember, anyone can read it anyway and copy it right from the screen with a screenshot. Also unencrypted text in apps can be read and extracted quite easily using a HexEditor!
Alternatively you can prepare the text in *.txt (unformatted) or html (formatted as you like) file format and just include it in your app. However, this is the easies way for others to just copy the file.
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.
Im doing my first localized project and I've been fighting with it for several hours with no luck.
I have to create an app that, based on the user selection, shows texts and images in different languages.
I've read most of Apple's documents on the matter but I cant make a simple example work.
This are my steps so far:
1) Create a new project.
2) Manually create a "en.lproj" directory in the projects folder.
3) Using TexEdit create file called "Localizable.strings" and store it in Unicode UTF-16. The file looks like this:
/*
Localizable.strings
Multilanguage02
Created by Gonzalo Floria on 5/6/10.
Copyright 2010 __MyCompanyName__. All rights reserved.
*/
"Hello" = "Hi";
"Goodbye" = "Bye";
4) I drag this file to the Resources Folder on XCode and it appear with the "subdir" "en" underneath it (with the dropdown triangle to the left). If I try to see it on XCode it looks all wrong, whit lots of ? symbols, but Im guessing thats because its a UTF-16 file. Right?
5) Now on my view did load I can access this strings like this:
NSString *translated;
translated = NSLocalizedString(#"Hello", #"User greetings");
NSLog(#"Translated text is %#",translated);
My problem is allowing the user to switch language. I have create an es.lproj with the Localizable.strings file (in Spanish), but I CANT access it.
I've tried this line:
[[NSUserDefaults standardUserDefaults] setObject: [NSArray arrayWithObjects:#"es", nil] forKey:#"AppleLanguages"];
But that only works the NEXT time you load the application. Is there no way to allow the user to switch languages while running the application??
Do I have to implement my own Dictionary files and forget all about NSLocalizableString family?
Thanks for ANY advice or pointers.
Gonso
There is already a discussion about that here.
Their suggestion is to create a sub-bundle and then use a method called NSLocalizedStringFromTableInBundle(...) (as described in the reference manual) to
get a localized string from a specific table in a bundle.
I am just giving you an hunch, I haven't tried but, I guess, this could be a good way to face your problem.