Detect and Customize UIWebView's "Unable to Read Document" error - iphone

I'm testing a UIWebview with a number of different document types - .xlsx, .jpg, etc. - and it opens most of them just fine. From time to time, I open a local file and this message appears right in the web view:
Unable to Read Document
An error occurred while reading the document
I'm not concerned with "why" this error occurs - it happens, for instance, when I feed the UIWebView a garbage file (intentionally). The problem is that I can't figure out how to detect "when" this happens. It doesn't trigger webView:didFailLoadWithError, it doesn't trigger an NSException (via #try & #catch), and when I inspect the document in webViewDidFinishLoad, webView.request.HTTPBody is null.
Anyone know how to detect when UIWebView can't display content?

just call uiwebview delegate method
- (void)webViewDidStartLoad:(UIWebView *)webView
{
}
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
NSString *html = [webView stringByEvaluatingJavaScriptFromString:
#"document.body.innerHTML"];
if ([html rangeOfString:#"Unable to Read Document."].location == NSNotFound) {
NSLog(#"NO Error");
}
else
{
// NSLog(#"File contains Error");
}
}

If inspecting the log does not show anything out of the ordinary, run it on the actual device. As a bit of help, my log says
Cannot find data converter callback for uti public.data
Failed to generate preview
which means that the simulator is failing. After being frustrated with this same problem, I went thru the whole final certification and installation and provisioning process, and too confirm that Word 97 and Pages / Pages.zip files containing text do indeed display just fine on the actual device. SOLVED. The simulator itself is broken, which is very...troubling, that this little note didn't seem to make it into the release notes, and also complicates development a tad bit. However, the work around is to select the Device in Xcode and deploy out, and it should work.

Related

Manage the UIWebView loading process

I use UIWebView to present data, and a spinner to show the loading process. The data is an .mp3 file from my server.
I start the spinner when I start loading the webView. Then there is a delay until the audio file starts playing. I need to Stop the spinner quite at that moment.
Notta big deal, but just in case - the loading code:
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://blablabla.mp3"]]];
Question: how could I catch the event when the webView is ready to play the audio (when there is enough data to start playing) ? I need it to stop the spinner.
WebViewDidFinishLoad is the only delegate method I could use and it's not good for me, because it notifies when ALL data is loaded. Even if I use it, it is not getting called when the audio file finishes loading (maybe it's not finishing, I don't know, I just see the loading progress gets to the end while the mp3 is playing). Just in case - the error:
Error Domain=NSURLErrorDomain Code=-999
"The operation couldn’t be completed. (NSURLErrorDomain error -999.)"
UserInfo=0x1d39d0 {NSErrorFailingURLKey=http://blablabla.mp3,
NSErrorFailingURLStringKey=http://blablabla.mp3}
Any help/tut/link is appreciated. Thanks in advance!
For that kind of control I would not recommend doing it via a webView - you can't get that level of interaction.
You may try da very popular ASIHTTPRequest class.
There is a [didReceiveData:] delegate, maybe suitable for your testing.
Don't worry it's easy !
the -999 for WebView is a normal error that apply when you are loading another content without letting the page really gets fully loaded.
so must of the time do that :
if([error code] != NSURLErrorCancelled) return;
To your javascript event detection :
Execute Objective-C methods from JS
This is unfortunately slightly more complex, because there isn't the same windowScriptObject property (and class) that exists on Mac OSX allowing complete communication between the two.
However, you can easily call from javascript custom-made URLs, like:
window.location = yourscheme://callfunction/parameter1/parameter2?parameter3=value
load a jquery library and use the document.ready event :
$(document).ready(function() {
window.location = yourscheme://callfunction/parameter1/parameter2?parameter3=value
});
And intercept it from Objective-C with this:
- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType {
NSURL *URL = [request URL];
if ([[URL scheme] isEqualToString:#"yourscheme"]) {
// parse the rest of the URL object and execute functions
}
}
This is not as clean as it should be (or by using windowScriptObject) but it works.
Last solution :
A Javascript Bridge
https://github.com/marcuswestin/WebViewJavascriptBridge
Enjoy
Instead of using the iOS activity indicator, implement the activity indicator inside the html. It is much simpler end also elegant solution. Just show/hide html element with animated gif image using the javascript.
Use a separate class to manage the NSUrlConnection for the retrieval and storage of the .mp3 file. You could use a NSNotificationCenter and the postNotification message or a delegate for when the file is ready to be played

Display encrypted file using QuickLook framework or UiDocumentInteractionController

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.

Opening tel: links from UIWebView

I've searched and searched, but I can't seem to find a fix for this problem.
For some reason, I can not get 'tel:' links to work in a UIWebView. When the links are clicked, the message "The URL can't be shown" appears. Clicking on the same link in Safari works perfectly and dials the number.
This problem started with iOS 5. These links worked perfectly in iOS 4.2 and 4.3.
I'm not sure what other information might be useful, so please let me know if I need to clarify.
Thanks!
EDIT:
Here is the actual code in use...
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
NSURL *url = request.URL;
if ([url.scheme isEqualToString:#"tel"]) {
return YES;
}
if (![url.scheme isEqualToString:#"http"] && ![url.scheme isEqualToString:#"https"]) {
if ([[UIApplication sharedApplication] canOpenURL:url]) {
[[UIApplication sharedApplication] openURL:url];
return NO; // Let OS handle this url
}
}
[NSThread detachNewThreadSelector:#selector(startBusy) toTarget:self withObject:nil];
return YES;
}
If I take out the first if statement, the number gets dialed immediately with no confirmation. I'd really like it to function the way it used to by giving an alert, giving you the option to hit either 'Call' or 'Cancel' before dialing the number.
If launching as an HTML link, the tel URL scheme will be opened if they appear as:
1-408-555-5555
If you are launching from a native URL string (meaning you coded this in Objective-C and are not serving it via a WebView), your URL string should look like this:
tel:1-408-555-5555
Note: This only works with iOS devices that have the Phone app installed (that means iPhone only). iPad & iPod Touch devices will display a warning message.
Note 2: Ensure the phone numbers you are passing do not contain spaces or other special characters (such as * and #).
Code Feedback
Based on your code, things are a bit clearer now. You comment about how nothing happens when you leave the first if statement in the shouldStartLoadWithRequest method (where you return YES). This is exactly the behavior you should see because your app is not the Phone app. Only the Phone app can handle the tel: URL scheme. By returning YES, you are telling the OS that your app will handle the phone call, but it cannot. You get the call when that conditional is removed because the next block, which checks if ([[UIApplication sharedApplication] canOpenURL:url]) allows the sharedApplication (which, in this case, is the Phone app) to launch the call.
How Things Work & What You Want
The OS is not going to handle showing the Call/Cancel alert dialog for you. That is up to you. It shows up in Safari because the Safari app's shouldStartLoadWithRequest method undoubtedly responds to the tel: scheme by showing a UIAlertView. Your conditional for if ([url.scheme isEqualToString:#"tel"]) should, when YES, trigger a UIAlertView with a Call and Cancel button. On Call, you will tell the sharedApplication to openURL; on Cancel, you will not issue the call & you will also want to return NO so your app does not attempt to loadWithRequest.
Self-Correcting Edit
To be fair about errors in my own thought process, I'm leaving my responses above.
I believe the Call/Cancel dialog is, in fact, a feature of the OS. Apologies for the inaccuracy.
I'd also erroneously glanced over your code's passing off URL handling to sharedApplication only occurring when the scheme was http or https.
After another look at the code, I wonder if, by any chance you have debug options on in Safari? I believe this prevents the alert from popping up. Also--just to double-check the obvious--you aren't trying this inside the simulator, correct? What happens if you remove the conditional check for http/https and just use the canOpenURL check?
However, aside from the error in my comments on the conditional & dialog itself, you still should not be returning YES. To make a phone call, you should only be able to pull that off by passing it to sharedApplication:openURL and ensuring you return NO because your app is not the Phone app. The only reason you'd want to return YES in this method is if your app is going to handle a tel: link in a special way that doesn't involve sending it to the Phone app.
If you created the UIWebView in a .xib, select the UIWebView and check its attributes in the Attribute Inspector. The first heading should be 'Web View', and under that it provides a list of checkboxes marked 'Detection'. Ensure that 'Phone Numbers' is checked.

How do you view errors in UIWebView loadHTMLString:?

If Apple encounters errors during loadHTMLString, they throw them on a separate thread, with no stacktrace (in Xcode4), and no output to the console.
How do you listen to these errors, debug them, and - ultimately - react to them?
(FYI - I'm using loadHTMLString because I need to load a mix of local and remote resources, and this method provides the only simple way to do it, AFAIAA)
EDIT: ...sorry, to be clear: There are different errors that Apple may encounter. For instance, if it gets an error trying to load an embedded resource (e.g. a CSS file), it won't count that as a "page failed to load", in fact it will report a successful page load.
IMHO ... that is the correct behaviour: if the HTML-parser is able to recover from the error, I don't want "page failed to load". But the errors are still important - they tell us why the page is rendering e.g. without a background image, or with broken images.
For debugging purposes you can attach Safari Web Inspector. To do so:
Make sure you have a version of Safari installed that supports SWI - so at least Safari 6 on Mac OS X 10.7.4.
On the device (or simulator) you're using to test the app, enable Settings > Safari > Advanced > "Web Inspector".
Launch Safari.
Enable Safari > Preferences > Advanced > "Show Develop menu in menu bar".
Run the app and, when the webview you're working with displays, go to Safari > Develop > [device name or "iPhone simulator"] > [web address - it will be listed under the app name]
It will show you what static files have been loaded by the page. (If you want to see Javascript errors, you'll need to attach as normal, set a breakpoint on all exceptions - in SWI, not just in Xcode! - then select the root-level page and CMD+R to reload it.)
As far as I can tell, the only way to do this programmatically (for instance, if you want to retry failed page loads) is with an NSURLProtocol subclass.
Although I haven't used loadHTMLString:, you should be able to register your controller as a UIWebViewDelegate and implement the – webView:didFailLoadWithError: method which should call whenever it fails.
Assign a delegate to your webview and use this method in the delegate.
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
NSLog(#"Error is %#",error);
}

Using a UIWebView with loadHTMLString/loadData breaks the back and forward buttons, workaround?

There's a known problem with embedded UIWebViews that if you load data into them using loadHTMLString or loadData, the canGoBack/canGoForward properties and goBack/goForward methods don't work. These only work when using loadRequest.
Since Safari's normal app cache doesn't work in embedded UIWebViews, creating a native app that effectively caches otherwise live content becomes impossible/unusable. That is, I can cache the contents of the HTML, Javascript, images, etc. and load them via loadHTMLString or loadData, but then the back and forward buttons don't work.
I could also use loadRequest and specify a file URL, but that breaks when it comes to communicating with the live site -- even if I specify a tag (because of cookie domain issues).
I have a work around that involves basically re-implementing the app cache using local store (and not having the native app do any caching itself), which is OK, but not really ideal. Are there any other work arounds/something I missed?
I am using the UIWebView's canGoBack to check to see if I'm at the first page of the history. If I am then I just call the little method I used to load the first page (displayLocalResource sets up the HTMLString and loads it into the webView). Here is a snippet:
//Implementing a back button
- (void)backOne:(id)sender{
if ([webView canGoBack]) {
// There's a valid webpage to go back to, so go there
[webView goBack];
} else {
// You've reached the end of the line, so reload your own data
[self displayLocalResource];
}
}
So do you download the HTML yourself, then pass it to UIWebView as a string? Why so? Do you modify it on the fly or something?
Maybe a custom URL schema would help? You use loadRequest with a schema of your own, which in turn works with HTTP and then feeds the webview whatever data you want?
I had a same problem. I tried manage the history, but it is error prone. Now I have discovered a better solution of this.
What you want to do is simply add a loadRequest to about:blank and make that as a placeholder for you before you call loadHTMLString/loadData. Then you are totally free from monitoring the history. The webview.canGoBack and canGoForward will just work. Of course, you will need a hack to handle go back to the placeholder about:blank. You can do that in webViewDidFinishLoad. Here is the code highlight:
In the function when you call loadHTMLString:
[weakSelf.fbWebView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:#"about:blank"]]];
[weakSelf.fbWebView loadHTMLString:stringResponse baseURL:url];
Code to handle goBack:
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
if ([webView.request.URL.absoluteString isEqualToString:#"about:blank"]
&& ![webView canGoBack] && [webView canGoForward]) {
[weakSelf.fbWebView loadHTMLString:stringResponse baseURL:url];
}
}
I think it is also possible expand this solution to handle those loadHTMLString that is not the first load. Just by having a Stack to record all the string response and insert an about:blank on each loadHTMLString. And pop the stack when each time go back to about:blank.
Could you fetch the content, save it to the local filesystem, point the webview to the local filesystem using file:// URLs, then intercept the link follows with shouldStartLoadWithRequest to fetch more to local fs, point webview at new local content, etc?
I've had good luck with UIWebView and file:/// URLs. Basically you'd be intercepting load requests, fetching stuff yourself, writing it to the local filesystem with rewritten URLs, then loading that into the browser.
There seems to be no way to load/save the browser history.
Loading the string into a temp file and using that as a URL request seems to cure this. It's something about loading the string directly that causes UIWebView not to see it as the home page you can navigate back to. This code worked for me:
//If you load the string like this, then "webView.canGoBack" never returns YES. It's documented frequently on the web.
//Loading the string as a URL instead seems to work better.
//[self.myWebView loadHTMLString:str baseURL:nil];
//write the string to a temp file
NSString *fileName = #"homepage.html";
NSURL *fileURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:fileName]];
NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];
[data writeToURL:fileURL atomically:NO];
//open that temp file in the UIWebView
[self.myWebView loadRequest:[NSURLRequest requestWithURL:fileURL]];
Use this to enable/disable the back button:
- (void)webViewDidFinishLoad:(UIWebView *)webView{
//this is to check if we're back at the root page.
if (webView.canGoBack == YES) {
self.backButton.enabled=YES;
}
else {
self.backButton.enabled=NO;
}
}