31

I need to know, when a web page has completely been loaded by UIWebView. I mean, really completely, when all redirects are done and dynamically loaded content is ready. I tried injecting javascript (querying for document.readyState == 'complete'), but that does not seem to be very reliable.

Is there, maybe, an event from the private api that will bring me the result?

Levon
  • 138,105
  • 33
  • 200
  • 191
Sebastian
  • 905
  • 2
  • 9
  • 21
  • I don't think that you need a private API. See my answer below... – gtmtg Jun 12 '12 at 11:54
  • I also added an answer that **does** use a private framework, but still may not get you rejected from the App Store. In any case, I would recommend using the estimatedProgress method only if the webViewDidFinishLoad method doesn't work... – gtmtg Jun 12 '12 at 12:00
  • You said you tried injecting javascript but it didn't seem reliable. In my experience it is, where/how are you injecting the code, when are you calling that code, when and how are you calling document.readyState? – Gruntcakes Jun 12 '12 at 15:04
  • 1
    The only solution (I found) which works ok: http://stackoverflow.com/questions/1662565/uiwebview-finished-loading-event/25620001#25620001 – sabiland Sep 02 '14 at 09:24

11 Answers11

48

I have been looking for the answer for this, and I got this idea from Sebastian's question. Somehow it works for me, maybe it will for those who encounter this issue.

I set a delegate to UIWebView and in the webViewDidFinishLoad, I detect if the webview has really finished loading by executing a Javascript.

- (void)webViewDidFinishLoad:(UIWebView *)webView {
    if ([[webView stringByEvaluatingJavaScriptFromString:@"document.readyState"] isEqualToString:@"complete"]) {
        // UIWebView object has fully loaded.
    }
}
Fedry Kemilau
  • 692
  • 6
  • 9
  • 2
    If your answer works, it seems the best answer I've seen all over stack overflow for this. But no one has voted for this answer in the 3 years its been here, so I suspicious... I will give it a shot. – Richard Venable Apr 16 '13 at 02:47
  • 1
    Great! But don't forget to add UIWebViewDelegate in the header, and [webViewImage setDelegate:self] in the implementation file where you define the UIWebView – DeZigny Jul 19 '13 at 15:20
  • This didn't work for me when trying to load the following URL: http://imgur.com/A9dlJpQ – olivaresF Aug 02 '13 at 22:45
  • 2
    For all six 'OnPageFinished' calls, the state was complete. So it did not work for me as a method to detect the last pageload event. – Hugo Logmans Sep 02 '13 at 14:06
  • This code is wrong. It will give you the readyState of the frame not the master document, which is what you need. If you use this, you'll still record multiple "finished" states. – jasonjwwilliams Mar 17 '14 at 20:34
  • When attempting to load http://www.cnn.com, document.readyState returns complete *three* times before the web page is actually complete. Each time, there are no outstanding webViewDidStartLoad's that have not been completed by webViewDidFinishLoad's. – Chris Prince Apr 23 '14 at 16:57
  • Same as Anthony here. – Patrick Bassut May 19 '14 at 04:18
  • 23
    To my understanding, the webViewDidFinishLoad only gets called once for each page load. So if the document.readyState is not "complete" when UIWebView thinks the load is complete. This delegate method won't ever be called again. So essentially we are getting dead code in this if block in this case. – Robert Kang Jul 31 '14 at 18:30
  • This is a really good idea: you no longer depend only by the framework and its delegates but you have also a server-side help. – Alessandro Ornano Jun 20 '17 at 13:41
22

My solution:

- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    if (!webView.isLoading) {
        // Tada
    }
}
tgf
  • 317
  • 4
  • 7
11

UIWebView's ability to execute Javascript is independent of whether a page has loaded or not. Its possible use stringByEvaluatingJavaScriptFromString: to execute Javascript before you have even made a call to load a page in UIWebView in the first place, or without loading a page at all.

Therefore I cannot understand how the answer in this link is accepted: webViewDidFinishLoad: Firing too soon? because calling any Javascript doesn't tell you anything at all (if they are calling some interesting Javascript, which is monitoring the dom for example, they don't make any mention of that and if there were they would because its important).

You could call some Javascript that, for example, examines the state of the dom or the state of the loaded page, and reports that back to the calling code however if it reports that the page has not loaded yet then what do you do? - you'll have to call it again a bit later, but how much later, when, where, how, how often, .... Polling is usually never a nice solution for anything.

The only way to know when the page has totally loaded and be accurate and in control of knowing exactly what what its in is to do it yourself - attach JavaScript event listeners into the page being loaded and get them to call your shouldStartLoadWithRequest: with some proprietary url. You could, for example, create a JS function to listen for a window load or dom ready event etc. depending upon if you need to know when the page has loaded, or if just the dom had loaded etc. Depending upon your needs.

If the web page is not your's then you can put this javascript into a file and inject it into every page you load.

How to create the javascript event listeners is standard javascript, nothing specially to do with iOS, for example here is the JavaScript to detect when the dom has loaded in a form that can be injected into the loading page from within the UIWebView and then result in a call to shouldStartLoadWithRequest: (this would invoke shouldStartLoadWithRequestwhen: the dom has finished loadeding, which is before the full page content has been displayed, to detect this just change the event listener).

var script = document.createElement('script');  
script.type = 'text/javascript';  
script.text = function DOMReady() {
    document.location.href = "mydomain://DOMIsReady";
}

function addScript()
{        
    document.getElementsByTagName('head')[0].appendChild(script);
    document.addEventListener('DOMContentLoaded', DOMReady, false);
}
addScript();
Community
  • 1
  • 1
Gruntcakes
  • 37,738
  • 44
  • 184
  • 378
  • 1
    The DOMContentLoaded doesn't wait until all resources (e.g., images) are loaded. E.g., see http://ie.microsoft.com/TEStdrive/HTML5/DOMContentLoaded/Default.html – Chris Prince Apr 23 '14 at 18:45
5

WKWebView calls didFinish delegate when DOM's readyState is interactive, which is too early for webpages havy loaded with javascripts. We need to wait for complete state. I've created such code:

func waitUntilFullyLoaded(_ completionHandler: @escaping () -> Void) {
    webView.evaluateJavaScript("document.readyState === 'complete'") { (evaluation, _) in
        if let fullyLoaded = evaluation as? Bool {
            if !fullyLoaded {
                DDLogInfo("Webview not fully loaded yet...")
                DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
                    self.waitUntilFullyLoaded(completionHandler)
                })
            } else {
                DDLogInfo("Webview fully loaded!")
                completionHandler()
            }
        }
    }
}

and then:

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    DDLogInfo("Webview claims that is fully loaded")

    waitUntilFullyLoaded { [weak self] in
        guard let unwrappedSelf = self else { return }

        unwrappedSelf.activityIndicator?.stopAnimating()
        unwrappedSelf.isFullLoaded = true
    }
}
konradowy
  • 1,572
  • 17
  • 27
4

Martin H's answer is on the right track, but it can still get fired multiple times.

What you need to do is add a Javascript onLoad listener to the page and keep track of whether you've already inserted the listener:

#define kPageLoadedFunction @"page_loaded"
#define kPageLoadedStatusVar @"page_status"
#define kPageLoadedScheme @"pageloaded"

- (NSString*) javascriptOnLoadCompletionHandler {
    return [NSString stringWithFormat:@"var %1$@ = function() {window.location.href = '%2$@:' + window.location.href; window.%3$@ = 'loaded'}; \
    \
    if (window.attachEvent) {window.attachEvent('onload', %1$@);} \
    else if (window.addEventListener) {window.addEventListener('load', %1$@, false);} \
    else {document.addEventListener('load', %1$@, false);}",kPageLoadedFunction,kPageLoadedScheme,kPageLoadedStatusVar];
}

- (BOOL) javascriptPageLoaded:(UIWebView*)browser {
    NSString* page_status = [browser stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"window.%@",kPageLoadedStatusVar]];
    return [page_status isEqualToString:@"loaded"];
}


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

    if(!_js_load_indicator_inserted && ![self javascriptPageLoaded:browser]) {
        _js_load_indicator_inserted = YES;
        [browser stringByEvaluatingJavaScriptFromString:[self javascriptOnLoadCompletionHandler]];
    } else
        _js_load_indicator_inserted = NO;
}

When the page is finished loading, the Javascript listener will trigger a new page load of the URL pageloaded:<URL that actually finished>. Then you just need to update shouldStartLoadWithRequest to look for that URL scheme:

- (BOOL)webView:(UIWebView *)browser shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    if([request.URL.scheme isEqualToString:kPageLoadedScheme]){
        _js_load_indicator_inserted = NO;
        NSString* loaded_url = [request.URL absoluteString];
        loaded_url = [loaded_url substringFromIndex:[loaded_url rangeOfString:@":"].location+1];

        <DO STUFF THAT SHOULD HAPPEN WHEN THE PAGE LOAD FINISHES.>

        return NO;
    }
}
jasonjwwilliams
  • 2,541
  • 3
  • 19
  • 14
  • This works for me. I made one tweak for dealing with non-HTML pages. I found that if the web view is loading non-HTML documents such as PDFs webViewDidFinishLoad: was only called once and installing the Javascript load completion handler in webViewDidFinishLoad: was too late for the page_loaded function to be triggered. I needed to install the load completion handler Javascript in webViewDidStartLoad:, so that by the time webViewDidFinishLoad: is called the first (and only) time page_loaded is already present and can trigger the sentinel page load. – user2067021 Jul 08 '14 at 02:09
  • Where is _js_load_indicator_inserted defined and set? I don't see this in the answer and I'm not sure how to use it without it. – kraftydevil Nov 17 '14 at 20:56
  • currently I am defining as follows: `BOOL _js_load_indicator_inserted = NO;`, however `request.URL.scheme` is never equal to kPageLoadedScheme (@"pageloaded") when `webView:shouldStartLoadWithRequest:navigationType:` is called. – kraftydevil Nov 17 '14 at 21:18
3

You could also use the accepted answer here: UIWebView - How to identify the "last" webViewDidFinishLoad message?...

Basically, use the estimatedProgress property and when that reaches 100, the page has finished loading... See this guide for help.

Although it does use a private framework, according to the guide there are some apps in the App Store that use it...

Community
  • 1
  • 1
gtmtg
  • 3,010
  • 2
  • 22
  • 35
  • yeah, i tried that and this did what i wanted. although i do not like very much to do these hackish things to gather so normal information like that. or the download status of a web page (which i retrieved through private apis like described in your second link) – Sebastian Jun 18 '12 at 18:49
  • My iOS app is just rejected because of using the "_documentView" API (that is needed to get estimatedProgress). – Tsuneo Yoshioka Jan 07 '13 at 21:07
  • @TsuneoYoshioka Yeah - as I said in my answer, the `_documentView` API is private and will get your app rejected. – gtmtg Jan 07 '13 at 21:14
  • I've no idea how this became the accepted answer. The correct answer is from Martin H. – Gruntcakes Apr 17 '14 at 04:42
1

Make your View Controller a delegate of the UIWebView. (You can either do this in Interface Builder, or by using this code in the viewDidLoad method:

[self.myWebView setDelegate:self];

Then use the method described in the accepted answer here: webViewDidFinishLoad: Firing too soon?.

The code:

-(void) webViewDidFinishLoad:(UIWebView *)webView
{
    NSString *javaScript = @"function myFunction(){return 1+1;}";
    [webView stringByEvaluatingJavaScriptFromString:javaScript];

    //Has fully loaded, do whatever you want here
}
Community
  • 1
  • 1
gtmtg
  • 3,010
  • 2
  • 22
  • 35
1

Thanks to JavaScript, new resources and new graphics can be loading forever-- determining when loading is "done", for a page of sufficient perversity, would amount to having a solution to the halting problem. You really can't know when the page is loaded.

If you can, just display the page, or an image of it, and use webViewDidLoad to update the image, and let the user descide when to cut things off-- or not.

Neal
  • 201
  • 2
  • 6
0

Here's a simplified version of @jasonjwwilliams answer.

#define JAVASCRIPT_TEST_VARIBLE @"window.IOSVariable"
#define JAVASCRIPT_TEST_FUNCTION @"IOSFunction"
#define LOAD_COMPLETED @"loaded"

// Put this in your init method
// In Javascript, first define the function, then attach it as a listener.
_javascriptListener = [NSString stringWithFormat:
    @"function %@() {\
         %@ = '%@';\
    };\
    window.addEventListener(\"load\", %@, false);",
        JAVASCRIPT_TEST_FUNCTION, JAVASCRIPT_TEST_VARIBLE, LOAD_COMPLETED, JAVASCRIPT_TEST_FUNCTION];

// Then use this:

- (void)webViewDidFinishLoad:(UIWebView *)webView;
{
    // Only want to attach the listener and function once.
    if (_javascriptListener) {
        [_webView stringByEvaluatingJavaScriptFromString:_javascriptListener];
        _javascriptListener = nil;
    }

    if ([[webView stringByEvaluatingJavaScriptFromString:JAVASCRIPT_TEST_VARIBLE] isEqualToString:LOAD_COMPLETED]) {
        NSLog(@"UIWebView object has fully loaded.");
    }
}
Chris Prince
  • 7,288
  • 2
  • 48
  • 66
  • Actually, on further testing, I'm seeing that this method isn't fully accurate either. Sometimes, in my testing (loading cnn.com), the output "UIWebView object has fully loaded." occurs more than once!! – Chris Prince Apr 23 '14 at 18:47
0

Since all these answers are old and webViewDidFinishLoad has been depreciated and many headaches later let me explain how to exactly know when a page has finished loading in a webkit (webview) with swift 4.2

class ViewController: UIViewController, WKNavigationDelegate {

    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!

    override func viewDidLoad() {

        webView.navigationDelegate = self

    }

    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {

    // it is done loading, do something!

    }

}

Of course I am missing some of the other default code so you are adding to your existing functions.

Cesar Bielich
  • 4,754
  • 9
  • 39
  • 81
-1

try something like it https://github.com/Buza/uiwebview-load-completion-tracker it's helped me for loading Google Street View

Vaskravchuk
  • 145
  • 2
  • 5