UIWebView - How to identify the "last" webViewDidFinishLoad message?
Asked Answered
L

10

23

The webViewDidFinishLoad message seems to be sent each time any object in the page has been loaded. Is there a way to determine that all loading of content is done?

Larder answered 25/5, 2009 at 23:27 Comment(1)
The accepted answer to this question and the answer with the most updates are incomplete. Look at this thread but ignore the accepted answer which just redirects to this one. #10996528Helle
K
9

Interesting, I wouldn't have thought it would work like that. Although I'm sure there are other ways to do it (is there a way to extract the URL from the webViewDidFinishLoad message so that you can see which one is the main page finishing loading?), the main thing I can think of is using the estimatedProgress to check the progress of the page and fire off whatever you want to do when it's 100% finished loading, which is what I do in my app. Google "iphone webview estimatedprogress" and click the first link for a guide I wrote on how to do this.

Update:

Please use phopkins' answer below instead of mine! Using private APIs in your apps is a bad idea and you will probably get rejected, and his solution is the right one.

Kimberliekimberlin answered 26/5, 2009 at 1:32 Comment(4)
I have similar problem in one of my apps & implemented AriX's solution - however, my question is whether Apple honestly allows this into the app store? One of the comments seems to suggest they don't (at least they didn't in 2009.) Has anyone had more recent experience with this?Irate
I believe that at some point, Apple allowed such apps into their store, but I can confirm that they currently do not. I submitted an app using this API in November, and it was rejected. Why does Apple still not allow developers to access the progress of a web view, four years after the launch of the iPhone SDK? Who knows.Kimberliekimberlin
Actually, my app is just rejected from AppStore because of using non-public "_documentView". Be careful. :-)Erlineerlinna
Googling "iphone webview estimatedprogress" returns you to this page. I don't think you really thought that linking strategy through. :)Crew
N
61

I'm guessing that iframes cause the webViewDidStartLoad / webViewDidFinishLoad pair.

The [webView isLoading] check mentioned as an answer didn't work for me; it returned false even after the first of two webViewDidFinishLoad calls. Instead, I keep track of the loading as follows:

- (void)webViewDidStartLoad:(UIWebView *)webView {
  webViewLoads_++;
}


- (void)webViewDidFinishLoad:(UIWebView *)webView {
  webViewLoads_--;

  if (webViewLoads_ > 0) {
    return;
  }

  …
}

- (void)webView:(UIWebView*)webView didFailLoadWithError:(NSError*)error {
  webViewLoads_--;
}

(Note this will only work if the start / finished pairs don't come serially, but in my experience so far that hasn't happened.)

Neoteric answered 9/5, 2010 at 3:54 Comment(8)
@phopkins add this :) (void)webView:(UIWebView*)webView didFailLoadWithError:(NSError*)error { -- webViewLoads_; }Demigod
I hate hacking to do silly workarounds like this - but this is still better than the accepted answer, which involves using private Apple APIs. Thanks for you answer.Afflux
@HyejuJung is right. Make sure to implement didFailLoadWithError: or you're balancing will get off and you'll be sad :(Behind
Did not work for me. The pending counter hit zero several times on its way to completion. I am downloading several different UIWebViews simultaneously.Banlieue
This works great (as long as you use all three delegate methods); so many sites nowadays seem to have 10-20+ extra calls for embedded iframes (sharing buttons, etc.), ajax calls, etc., and this tracking method helps my webViewDidFinishLoad code run once or twice instead of 10+ times. Thanks!Insufficient
Same as @BillCheswick above me, the pending counter hit zero several times on its way to completion, however I only use one UIWebView. Also the navigation might finish in webView:didFailLoadWithError: and the "on completion" code should be written in both methods.Casket
Same as Ches. This did not work for me at all. Le sigh.Dielectric
THIS IS NOT the correct way to do it. I think people should look at this link BUT NOT at the accepted answer there, look at the other answers. #10996528Helle
L
19

Check this one, it will definitely work for you:

- (void)webViewDidFinishLoad:(UIWebView *)webView {
    if ([[webView stringByEvaluatingJavaScriptFromString:@"document.readyState"] isEqualToString:@"complete"]) {
        // UIWebView object has fully loaded.
    }
}
Lucre answered 23/4, 2014 at 9:50 Comment(5)
Excellent! Non-hacky, and checks exactly the state we want.Benco
Unfortuntately this is not with out its own problems. Sometimes the webViewDidFinishLoad: method will fire because a resource has finished download but BEFORE the renderer has finished constructing the DOM/loading Javascript etc and therefore the document.readyState is NOT yet equal to "complete". The solution is probably to start a repeat timer from within the first webViewDidFinishLoad: call that evaluates the document.readyState every 0.5 seconds or so. Hardly elegant however.Pipsissewa
Didn't work for me. My state is always interactive and never complete.Adal
perfect and the only thing that works ! this should be the accepted answer.Varga
Used the NSTimer solution (with a 0.1 interval). It's not nice, but does the job. Will put the code in a separate answer, for lazy copy-pasta like me :)Antalkali
K
9

Interesting, I wouldn't have thought it would work like that. Although I'm sure there are other ways to do it (is there a way to extract the URL from the webViewDidFinishLoad message so that you can see which one is the main page finishing loading?), the main thing I can think of is using the estimatedProgress to check the progress of the page and fire off whatever you want to do when it's 100% finished loading, which is what I do in my app. Google "iphone webview estimatedprogress" and click the first link for a guide I wrote on how to do this.

Update:

Please use phopkins' answer below instead of mine! Using private APIs in your apps is a bad idea and you will probably get rejected, and his solution is the right one.

Kimberliekimberlin answered 26/5, 2009 at 1:32 Comment(4)
I have similar problem in one of my apps & implemented AriX's solution - however, my question is whether Apple honestly allows this into the app store? One of the comments seems to suggest they don't (at least they didn't in 2009.) Has anyone had more recent experience with this?Irate
I believe that at some point, Apple allowed such apps into their store, but I can confirm that they currently do not. I submitted an app using this API in November, and it was rejected. Why does Apple still not allow developers to access the progress of a web view, four years after the launch of the iPhone SDK? Who knows.Kimberliekimberlin
Actually, my app is just rejected from AppStore because of using non-public "_documentView". Be careful. :-)Erlineerlinna
Googling "iphone webview estimatedprogress" returns you to this page. I don't think you really thought that linking strategy through. :)Crew
V
1

Another way to monitor load progress with less control is to observe the WebViewProgressEstimateChangedNotification, WebViewProgressFinishedNotification, and WebViewProgressStartedNotification notifications. For example, you could observe these notifications to implement a simple progress indicator in your application. You update the progress indicator by invoking the estimatedProgress method to get an estimate of the amount of content that is currently loaded.

from http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/WebKit/Classes/WebView_Class/Reference/Reference.html

Viquelia answered 30/8, 2012 at 8:22 Comment(0)
D
1

You can also use this short Category I wrote that adds blocks into UIWebView and also lets you choose between default UIWebView behavior (getting notified after each object load), or the "expected" behavior (getting notified only when the page has fully loaded).

https://github.com/freak4pc/UIWebView-Blocks

Desiderate answered 2/1, 2013 at 10:23 Comment(3)
This did not work for me by blowing up with a signal (SIGTRAP, I believe). Though, I read the code and used its logic on mine, and it worked fine.Obtain
Weird, works fine for me, not sure what was your exact usage but feel free to write it here if you want to. Cheers.Desiderate
If there is any problem please feel free to pull your copy and add a fix :)Desiderate
S
1

I needed to capture a variable from the page which was not fully loaded yet.

This worked for me:

- (void) webViewDidFinishLoad:(UIWebView *)webView {
    NSString *valueID = [webView stringByEvaluatingJavaScriptFromString:@"document.valueID;"];

    if([valueID isEqualToString:@""]){
        [webView reload];
        return;
    }

    //page loaded
}
Schoolmarm answered 30/1, 2014 at 13:14 Comment(0)
B
1

All of the options did not really work for my use case. I used phopkins example slightly modified. I found that if the HTML loaded into the webview contained an image that would be a separate request that happened serially so we have to account for that and I did that with a timer. Not the best solution but it seems to work.:

- (void)webViewActuallyFinished {
     _webViewLoads--;

    if (_webViewLoads > 0) {
        return;
    }

    //Actually done loading

}

- (void)webViewDidStartLoad:(UIWebView *)webView {
    _webViewLoads++;
}


- (void)webViewDidFinishLoad:(UIWebView *)webView {


    [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(webViewActuallyFinished) userInfo:nil repeats:NO];
}

- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
  _webViewLoads--;
}
Bipolar answered 13/2, 2014 at 22:20 Comment(1)
This worked for me. I tried using the javascript readyState and that didn't work. I don't like the fact of using a timer for this, but it works. This wouldn't be needed if webView's didFinishLoad was actually didFinishLoad.Hoe
T
0

hi it may be a bit far back already but i hope this helps.

i just used a notification when it enters the webViewDidFinishLoad method so that i can capture one instance of the method and then i'll detect the notification and do my logic from there.

hope this helps. it does not capture the last called instance of the webViewDidFinishLoad method, but allows you to do something once it has been called and not be repeated calls to that particular method (eg. showing a button) too.

*********EDIT*********

i did a test and managed to test it out. it actually works and the method called from the notification will be called after the full page has been loaded.

alternatively, i think you can do a counter on the delegate method webViewDidStartLoad and also on webViewDidFinishLoad to make sure that they are the same before you run your codes. this though, is an ugly hack as we will never know how many times it will be called unless like me, you are loading a html feed and can add a JavaScript to check how many elements there are on the page that you are loading. i'm just sharing some of the methods i have tried to solve this. hope it helps.

feedback is encouraged. thanks!

Teem answered 9/5, 2012 at 4:5 Comment(2)
What's the difference beween putting your code in webViewDidFinishLoad and making a notification in webViewDidFinishLoad to call another method?Guildhall
depends on what your code does actually. notification fires off another bunch of codes, so if you already have a method or something that is required to run separately, or concurrently, then i guess it might be better to call another method then. if not it really depends what your code does i guess.Teem
A
0

Here's what I use to show a spinner while DOM loads, built on top of @Vinod's answer.

Note that between webViewDidStartLoad and webViewDidFinishLoad the readyState is completed from the previous request (if any), that's why the polling should begin after webViewDidFinishLoad.

readyState possible values are loading, interactive or completed (and maybe nil ?)

- (void)webViewDidStartLoad:(UIWebView *)webView {
    [self spinnerOn];
}

- (void)webViewDidFinishLoad:(UIWebView *)webView {
    [self startDOMCompletionPolling];
}

- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
    [self startDOMCompletionPolling];
}


- (void)startDOMCompletionPolling {
    if (self.domCompletionListener) {
        [self.domCompletionListener invalidate];
    }
    self.domCompletionListener = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(checkDOMCompletion) userInfo:nil repeats:YES];
}

- (void)checkDOMCompletion {
    NSString *readyState = [self.webView stringByEvaluatingJavaScriptFromString:@"document.readyState"];

    if (readyState != nil) {
        if (readyState.length > 0) {
            if ([readyState isEqualToString:@"loading"]) { //keep polling
                return;
            }
        }
    }
    //'completed', 'interactive', nil, others -> hide spinner
    [self.domCompletionListener invalidate];
    [self spinnerOff];
}
Antalkali answered 15/3, 2016 at 13:47 Comment(0)
S
-7

The way I do it is this:

- (void)webViewDidFinishLoad:(UIWebView *)webview  {
    if (webview.loading)
    return;
    // now really done loading code goes next
    [logic]
}
Shad answered 21/6, 2009 at 17:22 Comment(1)
It definitely doesn't work. It is false each time "webViewDidFinishLoad" is called. But then, it still get called multiple times. Yes, 2 starts and THREE finishes. Example: Starting Load -- article 23 Reasons to Get Out There 1, Starting Load -- article 23 Reasons to Get Out There 1, Finish Load -- article 23 Reasons to Get Out There 0, Finish Load -- article 23 Reasons to Get Out There 0, Finish Load -- article 23 Reasons to Get Out There 0Usn

© 2022 - 2024 — McMap. All rights reserved.