31
votes

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?

11
I don't think that you need a private API. See my answer below...gtmtg
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
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
The only solution (I found) which works ok: stackoverflow.com/questions/1662565/…sabiland

11 Answers

48
votes

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.
    }
}
22
votes

My solution:

- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    if (!webView.isLoading) {
        // Tada
    }
}
11
votes

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();
4
votes

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;
    }
}
4
votes

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
    }
}
3
votes

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...

1
votes

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
}
1
votes

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.

0
votes

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.");
    }
}
0
votes

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.

-1
votes

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