How to determine iFrame finished loading in UIWebView - javascript

In my app, I need a way to tell when my webview is finished loading. It is very easy to do that if content is html. However, my content source is javascript with iFrame inside, it will cause UIWebView finishedLoad method called several times. Is there anyway I can check to see if the iframe is finished loading?

The only foolproof method I've found is to listen to the three methods that say it's about to start loading, and that it finished loading (plus equivalent for "failed"), and to manually maintain a count for each.
i.e. something like:
int outstandingRequests;
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
outstandingRequests++;
}
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
outstandingRequests--;
if( outstandingRequests < 1 )
viewLoadingPleaseWait.hidden = TRUE;
}
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
outstandingRequests--;
}
I've also usually had to overrid the didFail method - Apple incorrectly uses this to report that e.g. the YouTube player has taken over the handling of a YouTube video. It's NOT a fail, it's a "page was loaded by a different application".
e.g.
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
outstandingRequests--;
if( [error code] == NSURLErrorCancelled )
{
NSLog(#"[%#] ...webView CANCELLED loading", [self class] );
}
else if( [[error domain] isEqualToString:#"WebKitErrorDomain"]
&& [error code] == 204)
{
// no worries ... YouTube, iCal, or etc took over the page load
}
}

(If it is the same domain) you can call a function (in the parent) from the iframe.
top.iFrameLoaded();

To tell if a webview is loading, you can use
webView.isLoading
to check if all the frames of a webview are done loading. No counting the number of iframes necessary. Simply check if isLoading is true inside webViewDidFinishLoad:
http://developer.apple.com/library/ios/#documentation/uikit/reference/UIWebView_Class/Reference/Reference.html

Related

UIWebView in UITableViewCell only completes load in first cell

I am using a UIWebView as the accessory view in each UITableViewCell in a UITable in order to display a tiny Openlayers map in each table view. When I first display the table view, the UIWebView in the first cell displays the map fine, but the other UIWebViews in all the other cells are empty.
If I scroll the table up and down, then the other cells all load fine when they come back into view, each with the respective different little maps.
The UIWebViews each load an initial HMTL document on init, and this document includes the standard Openlayers ol.js javascript.
The accessory view UIView subclass is the delegate of the UIWebView and waits for it to load, before sending it another javascript to draw the map features.
Logging in this delegate method reveals that the web view loaded property is YES/TRUE, but the javascript readyState is still loading.
Why would this be? How can I get this working as expected?
The webView is told to load the generic HTML/javascript via a call to loadHTMLString:baseURL: from within the drawRect: method. Then the webViewDidFinishLoad: delegate method is used to check when loading is finished, so that the cell-specific javascript can be run after the generic HTML/javascript is complete.
The relevant code is:
- (void)drawRect:(CGRect)rect {
NSString *path = [NSBundle pathForResource:#"style" ofType:#"html" inDirectory:[[NSBundle mainBundle] bundlePath]];
NSString *html = [NSString stringWithContentsOfFile:path usedEncoding:nil error:nil];
[_webView loadHTMLString:html baseURL:[NSURL fileURLWithPath:[path stringByDeletingLastPathComponent] isDirectory:YES]];
}
- (void)webViewDidFinishLoad:(UIWebView *)webView {
if ( ! _webView.loading ) {
if ( [[webView stringByEvaluatingJavaScriptFromString:#"document.readyState"] isEqualToString:#"complete"] ) {
NSLog(#"All good");
[self drawFeatures];
} else {
NSLog(#"ERROR: webView loaded, readyState '%#'", [webView stringByEvaluatingJavaScriptFromString:#"document.readyState"]);
//[self performSelector:#selector(drawFeatures) withObject:nil afterDelay:0.5];
}
}
}
( [self drawFeatures] assembles some javascript and sends it to the UIWebView to run, and it relies on the standard Openlayers (ol.js) javascript having completed in initial HTML web page.).
The output is:
2017-02-26 15:15:00.071 myapp[50443:25603667] ERROR: webView loaded, readyState 'loading'
2017-02-26 15:15:00.093 myapp[50443:25603667] ERROR: webView loaded, readyState 'loading'
2017-02-26 15:15:00.116 myapp[50443:25603667] ERROR: webView loaded, readyState 'loading'
2017-02-26 15:15:00.132 myapp[50443:25603667] ERROR: webView loaded, readyState 'loading'
2017-02-26 15:15:00.148 myapp[50443:25603667] ERROR: webView loaded, readyState 'loading'
2017-02-26 15:15:00.166 myapp[50443:25603667] ERROR: webView loaded, readyState 'loading'
2017-02-26 15:15:00.190 myapp[50443:25603667] ERROR: webView loaded, readyState 'loading'
2017-02-26 15:15:00.208 myapp[50443:25603667] ERROR: webView loaded, readyState 'loading'
2017-02-26 15:15:00.226 myapp[50443:25603667] ERROR: webView loaded, readyState 'loading'
2017-02-26 15:15:00.245 myapp[50443:25603667] ERROR: webView loaded, readyState 'loading'
2017-02-26 15:15:00.262 myapp[50443:25603667] ERROR: webView loaded, readyState 'loading'
2017-02-26 15:15:00.292 myapp[50443:25603667] All good
As you can see from this output, the isLoaded state for most of the cells is true BEFORE isLoaded is true for the FIRST CELL (which comes up last after really truly being loaded and actually ready). This implies that the web view thinks it's loaded when it's really not.
The loading error delegate method never gets called, so there appears to be no HTML/UIWebView errors:
-(void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
NSLog(#"ERROR loading style view HTML:\n%#", [error description]);
}
Note that if I change the delegate method to check only the javascript readyState, and ignore the isLoading value, I never get a readyState of complete. Just loading. Presumably, this is because the web view things it is finished loading, and so this delegate method doesn't get called again.
WORK AROUND THAT WORKS:
I can work around the problem by uncommenting the performSelector:withObject:afterDelay: that is commented out in the delegate method above. This indicates that the HTML does eventually load OK, but that the delegate method never gets called after it really is ready.
How can I get this to work as expected without resorting to a dodgy workaround of perform-with-delay?
UPDATE TRYING AN ALTERNATIVE METHOD
Here's another version of trying to get this working, based partly on the ideas from #Ricowere's answer (and similar to what I started with before going for the drawRect: hack
The HTML loading is now in my UIWebView subclass method. In this case, the appropriate method is when I set a property of that UIWebView subclass:
- (void)setStyle:(RenderStyle *)style {
_style = style;
...
...
NSString *path = [NSBundle pathForResource:#"style" ofType:#"html" inDirectory:[[NSBundle mainBundle] bundlePath]];
NSString *html = [NSString stringWithContentsOfFile:path usedEncoding:nil error:nil];
[self loadHTMLString:html baseURL:[NSURL fileURLWithPath:[path stringByDeletingLastPathComponent] isDirectory:YES]];
}
Then in the table view delegate, each time a table view cell is dequeued, add a UIWebView subclass view to it as an accessory view, and then for that UIWebView subclass view, call the property method that loads the HTML:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"styleCell"];
if ( cell == nil ) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"styleCell"];
...
...
}
...
...
StyleViewUIWebViewSubclass *styleView = [[StyleViewUIWebViewSubclass alloc] initWithFrame:frame];
cell.accessoryView = styleView;
styleView.style = [[RenderStyle alloc] initWithString:styles[styleName]];
return cell;
}
However, this still has the same problem. The perform-with-delay work around above is still required to get it to work.
Firstly, remove the code you have in the drawRect. That approach is completely wrong, you're putting bussiness logic in a drawing method. (Which is executed once, and then the cells are reused)
Here, I show you a simple approach to tackle this.
class CellExample: UITableViewCell, UIWebViewDelegate {
var accessoryWebView: UIWebView? {
return accessoryView as? UIWebView
}
//If you're dequeuing the cell from IB.
override func awakeFromNib() {
// Set the frame you need or do this through autolayout.
let webView = UIWebView(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
accessoryView = webView
}
//You have to call this everytime a cell is dequeued
//(Then the content is properly displayed again.)
func load(url: URL) {
accessoryWebView?.delegate = self
//Load the request or the HTML content as you need.
//accessoryWebView?.loadHTMLString("whatever", baseURL: URL(string: "whatever")!)
// accessoryWebView?.loadRequest(URLRequest(url: url,
// cachePolicy: .returnCacheDataElseLoad,
// timeoutInterval: 10 ))
}
override func prepareForReuse() {
super.prepareForReuse()
//If the cell is reused 'restart' the stack.
accessoryWebView?.delegate = nil
accessoryWebView?.stopLoading()
}
func webViewDidFinishLoad(_ webView: UIWebView) {
// Do the stuff you need about the javascript.
}
}
Thanks to some tips from #Ricowere I got this working. My solution is based on the ideas from #Ricowere answer but using a UITableView delegate instead of a UITableViewCell subclass, and doing it all in Objective C instead of Swift.
Here's what ended up working for me...
In my WKWebView subclass:
- (id)initWithFrame:(CGRect)frame {
if ( self = [super initWithFrame:frame] ) {
...
self.navigationDelegate = self;
}
return self;
}
- (void)setStyle:(RenderStyle *)style {
_style = style;
NSURL *url = [NSBundle URLForResource:#"style" withExtension:#"html" subdirectory:nil inBundleWithURL:[[NSBundle mainBundle] bundleURL]];
[self loadFileURL:url allowingReadAccessToURL:[url URLByDeletingLastPathComponent]];
}
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
[self drawFeatures]; // This line runs the secondary instance-dependant javascript that relies on the generic javascript already having been completed
}
Then in my UITableViewDelegate:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"styleCell"];
if ( cell == nil ) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"styleCell"];
...
WKWebViewSubclass *styleView = [[WKWebViewSubclass alloc] initWithFrame:frame];
cell.accessoryView = styleView;
}
...
((WKWebViewSubclass *)cell.accessoryView).style = [[RenderStyle alloc] initWithString:styleString];
return cell;
}
It seems to run a little slower than the UIWebView, which is odd, but at least it works without errors, and without any dodgy work arounds.

webViewDidFinishLoad not calling when having javascript content in webpage

I'm using UIWebView in a project. Sometimes it doesn't call the webViewDidFinishLoad method. Because the web page have some javascripts. The method
- (BOOL) webView: (UIWebView *) webView shouldStartLoadWithRequest: (NSURLRequest *) request navigationType: (UIWebViewNavigationType) navigationType
is getting call but webViewDidFinishLoad doesn't. I want to catch that method. Because I'm start an animation when the webview start loading. Then I want to stop this animation when it finished. It's not working with websites having javascript content. Any one have an idea please?
Thanks
webViewDidFinishLoad method gets called when the UIWebView has finished loading the url, in your case since you are calling a javascript, it doesn't load your content, it just calls a javascript function in your already loaded webview. But YES you can catch the javascript actions in the same method you stated as below
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
NSString *url = [[request URL] absoluteString];
if(navigationType == UIWebViewNavigationTypeLinkClicked){
if([url rangeOfString:#"SOMETHING"].length > 0 ){
//if your action returns something appended in your url like the anchors work
//DO YOUR STUFFS
}
}
return TRUE;
}
apparently, the webview in ios checks specifically for "iframe" tags in the html's dom and in case it finds, it creates events based on that tag as well. therefore, a simple however a patchy solution would be adding the following code in the part of the dom that is changing (for example, in the actual template of each section):
<iframe></iframe>
did the trick for me..

User Interaction Enabled Javascript

I am writing a Javascript application and am going to wrap it in a native iOS application. I would like to block user interaction on the UIWebView containing the JS app for a second or two following an event.
Normally I would use the self.webView.userinteractionenabled = NO but the event is triggered in Javascript. How can I block the user from interacting with the web view?
Guessing return false on a touch event of some sort? It's scrolling that I want to block.
Thanks!
When the event occurs in your Javascript code you can send a message to the native wrapper by using the following method:
Set up the following UIWebViewDelegate method (don't forget to set the delegate for the UIWebView):
- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType {
NSURL *url = [request URL];
if ([[url scheme] isEqualToString:#"block"]) {
// do the your blocking code here
return NO;
}
return YES;
}
Now when your event happens, call the delegate method from your javascript code:
window.location.href = "block://";

Touches in a UIWebView

I'm developing an iOS app with a UIWebView instance.
If the user touches an object in a webpage shown by the web view, how can I extract metadata regarding the object touched (such as an "id" for an HTML element) over in Objective C land?
[I'm not interested in whether the web view was touched or not, I'm only interested in what part of the page was touched and being able to act on this.]
You probably need to do this in JavaScript land. In your webview set up some javascript to monitor the click (e.g. element.onclick = function() { ... }, or use jQuery if that's easier).
Now you can call out to the native code in your app by using a made up url and then intercepting it using the webview delegate, e.g.
//in your web page, in javascript
myDiv.onclick = function() { document.location.href = 'http://madeupdomain/' + this.id};
//webview delegate in cocoa
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSURL *URL = [request URL];
NSString *host = URL.host;
if ([host isEqualToString:#"madeupdomain"])
{
NSString *theDivID = URL.path;
//now do something based on the div id value
return NO;
}
return YES;
}
This is essentially how frameworks like PhoneGap communicate between the webview and the native code.

Detecting JavaScript errors in a UIWebView [duplicate]

I need to have my iPhone Objective-C code catch Javascript errors in a UIWebView. That includes uncaught exceptions, syntax errors when loading files, undefined variable references, etc.
This is for a development environment, so it doesn't need to be SDK-kosher. In fact, it only really needs to work on the simulator.
I've already found used some of the hidden WebKit tricks to e.g. expose Obj-C objects to JS and to intercept alert popups, but this one is still eluding me.
[NOTE: after posting this I did find one way using a debugging delegate. Is there a way with lower overhead, using the error console / web inspector?]
I have now found one way using the script debugger hooks in WebView (note, NOT UIWebView). I first had to subclass UIWebView and add a method like this:
- (void)webView:(id)webView windowScriptObjectAvailable:(id)newWindowScriptObject {
// save these goodies
windowScriptObject = newWindowScriptObject;
privateWebView = webView;
if (scriptDebuggingEnabled) {
[webView setScriptDebugDelegate:[[YourScriptDebugDelegate alloc] init]];
}
}
Next you should create a YourScriptDebugDelegate class that contains methods like these:
// in YourScriptDebugDelegate
- (void)webView:(WebView *)webView didParseSource:(NSString *)source
baseLineNumber:(unsigned)lineNumber
fromURL:(NSURL *)url
sourceId:(int)sid
forWebFrame:(WebFrame *)webFrame
{
NSLog(#"NSDD: called didParseSource: sid=%d, url=%#", sid, url);
}
// some source failed to parse
- (void)webView:(WebView *)webView failedToParseSource:(NSString *)source
baseLineNumber:(unsigned)lineNumber
fromURL:(NSURL *)url
withError:(NSError *)error
forWebFrame:(WebFrame *)webFrame
{
NSLog(#"NSDD: called failedToParseSource: url=%# line=%d error=%#\nsource=%#", url, lineNumber, error, source);
}
- (void)webView:(WebView *)webView exceptionWasRaised:(WebScriptCallFrame *)frame
sourceId:(int)sid
line:(int)lineno
forWebFrame:(WebFrame *)webFrame
{
NSLog(#"NSDD: exception: sid=%d line=%d function=%#, caller=%#, exception=%#",
sid, lineno, [frame functionName], [frame caller], [frame exception]);
}
There is probably a large runtime impact for this, as the debug delegate can also supply methods to be called for entering and exiting a stack frame, and for executing each line of code.
See http://www.koders.com/noncode/fid7DE7ECEB052C3531743728D41A233A951C79E0AE.aspx for the Objective-C++ definition of WebScriptDebugDelegate.
Those other methods:
// just entered a stack frame (i.e. called a function, or started global scope)
- (void)webView:(WebView *)webView didEnterCallFrame:(WebScriptCallFrame *)frame
sourceId:(int)sid
line:(int)lineno
forWebFrame:(WebFrame *)webFrame;
// about to execute some code
- (void)webView:(WebView *)webView willExecuteStatement:(WebScriptCallFrame *)frame
sourceId:(int)sid
line:(int)lineno
forWebFrame:(WebFrame *)webFrame;
// about to leave a stack frame (i.e. return from a function)
- (void)webView:(WebView *)webView willLeaveCallFrame:(WebScriptCallFrame *)frame
sourceId:(int)sid
line:(int)lineno
forWebFrame:(WebFrame *)webFrame;
Note that this is all hidden away in a private framework, so don't try to put this in code you submit to the App Store, and be prepared for some hackery to get it to work.
I created a nice little drop-in category that you can add to your project...
It is based on Robert Sanders solution. Kudos.
You can dowload it here:
UIWebView+Debug
This should make it a lot easier to debug you UIWebView :)
I used the great solution proposed from Robert Sanders: How can my iPhone Objective-C code get notified of Javascript errors in a UIWebView?
That hook for webkit works fine also on iPhone. Instead of standard UIWebView I allocated derived MyUIWebView. I needed also to define hidden classes inside MyWebScriptObjectDelegate.h:
#class WebView;
#class WebFrame;
#class WebScriptCallFrame;
Within the ios sdk 4.1 the function:
- (void)webView:(id)webView windowScriptObjectAvailable:(id)newWindowScriptObject
is deprecated and instead of it I used the function:
- (void)webView:(id)sender didClearWindowObject:(id)windowObject forFrame:(WebFrame*)frame
Also, I get some annoying warnings like "NSObject may not respond -windowScriptObject" because the class interface is hidden. I ignore them and it works nice.
One way that works during development if you have Safari v 6+ (I'm uncertain what iOS version you need) is to use the Safari development tools and hook into the UIWebView through it.
In Safari: Enable the Develop Menu (Preferences > Advanced > Show Develop menu in menu bar)
Plug your phone into the computer via the cable.
List item
Load up the app (either through xcode or just launch it) and go to the screen you want to debug.
Back in Safari, open the Develop menu, look for the name of your device in that menu (mine is called iPhone 5), should be right under User Agent.
Select it and you should see a drop down of the web views currently visible in your app.
If you have more than one webview on the screen you can try to tell them apart by rolling over the name of the app in the develop menu. The corresponding UIWebView will turn blue.
Select the name of the app, the develop window opens and you can inspect the console. You can even issue JS commands through it.
Straight Forward Way: Put this code on top of your controller/view that is using the UIWebView
#ifdef DEBUG
#interface DebugWebDelegate : NSObject
#end
#implementation DebugWebDelegate
#class WebView;
#class WebScriptCallFrame;
#class WebFrame;
- (void)webView:(WebView *)webView exceptionWasRaised:(WebScriptCallFrame *)frame
sourceId:(int)sid
line:(int)lineno
forWebFrame:(WebFrame *)webFrame
{
NSLog(#"NSDD: exception: sid=%d line=%d function=%#, caller=%#, exception=%#",
sid, lineno, [frame functionName], [frame caller], [frame exception]);
}
#end
#interface DebugWebView : UIWebView
id windowScriptObject;
id privateWebView;
#end
#implementation DebugWebView
- (void)webView:(id)sender didClearWindowObject:(id)windowObject forFrame:(WebFrame*)frame
{
[sender setScriptDebugDelegate:[[DebugWebDelegate alloc] init]];
}
#end
#endif
And then instantiate it like this:
#ifdef DEBUG
myWebview = [[DebugWebView alloc] initWithFrame:frame];
#else
myWebview = [[UIWebView alloc] initWithFrame:frame];
#endif
Using #ifdef DEBUG ensures that it doesn't go in the release build, but I would also recommend commenting it out when you're not using it since it has a performance impact. Credit goes to Robert Sanders and Prcela for the original code
Also if using ARC you may need to add "-fno-objc-arc" to prevent some build errors.
I have created an SDK kosher error reporter that includes:
The error message
The name of the file the error happens in
The line number the error happens on
The JavaScript callstack including parameters passed
It is part of the QuickConnectiPhone framework available from the sourceForge project
There is even an example application that shows how to send an error message to the Xcode terminal.
All you need to do is to surround your JavaScript code, including function definitions, etc. with try catch. It should look like this.
try{
//put your code here
}
catch(err){
logError(err);
}
It doesn't work really well with compilation errors but works with all others. Even anonymous functions.
The development blog is here
is here and includes links to the wiki, sourceForge, the google group, and twitter. Maybe this would help you out.
I have done this in firmware 1.x but not 2.x.
Here is the code I used in 1.x, it should at least help you on your way.
// Dismiss Javascript alerts and telephone confirms
/*- (void)alertSheet:(UIAlertSheet*)sheet buttonClicked:(int)button
{
if (button == 1)
{
[sheet setContext: nil];
}
[sheet dismiss];
}*/
// Javascript errors and logs
- (void) webView: (WebView*)webView addMessageToConsole: (NSDictionary*)dictionary
{
NSLog(#"Javascript log: %#", dictionary);
}
// Javascript alerts
- (void) webView: (WebView*)webView runJavaScriptAlertPanelWithMessage: (NSString*) message initiatedByFrame: (WebFrame*) frame
{
NSLog(#"Javascript Alert: %#", message);
UIAlertSheet *alertSheet = [[UIAlertSheet alloc] init];
[alertSheet setTitle: #"Javascript Alert"];
[alertSheet addButtonWithTitle: #"OK"];
[alertSheet setBodyText:message];
[alertSheet setDelegate: self];
[alertSheet setContext: self];
[alertSheet popupAlertAnimated:YES];
}
See exception handling in iOS7:
http://www.bignerdranch.com/blog/javascriptcore-example/
[context setExceptionHandler:^(JSContext *context, JSValue *value) {
NSLog(#"%#", value);
}];
First setup WebViewJavascriptBridge ,
then override console.error function.
In javascript
window.originConsoleError = console.error;
console.error = (msg) => {
window.originConsoleError(msg);
bridge.callHandler("sendConsoleLogToNative", {
action:action,
message:message
}, null)
};
In Objective-C
[self.bridge registerHandler:#"sendConsoleLogToNative" handler:^(id data, WVJBResponseCallback responseCallback) {
NSString *action = data[#"action"];
NSString *msg = data[#"message"];
if (isStringValid(action)){
if ([#"console.error" isEqualToString:action]){
NSLog(#"JS error :%#",msg);
}
}
}];
A simpler solution for some cases might be to just add Firebug Lite to the Web page.

Categories

Resources