38

Goal: To take a screenshot of WKWebView after the website finished loading

Method employed:

  • Defined a WKWebView var in UIViewController
  • Created an extension method called screen capture() that takes image of WKWebView

  • Made my UIViewController to implement WKNavigationDelegate

  • Set the wkwebview.navigationDelegate = self ( in the UIViewController init)

  • Implemented the didFinishNavigation delegation func in UIViewcontroller to call screen capture extension method for WKWebView

func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {
    let img = webView.screenCapture()
}

Questions:

  • When I debug i simulator, I notice that the control reaches the didFinishNavigation() func even though the website has not yet rendered in the WKWebView
  • Correspondingly the image screenshot taken is a white blob.

What am I missing here? I looked at all possible delegate functions for WKWebView and nothing else seem to represent the completion of content loading in WKWebView. Would appreciate help on if there is a work around


Update: Adding screenshot code that I am using to take a screenshot for web view

class func captureEntireUIWebViewImage(webView: WKWebView) -> UIImage? {

    var webViewFrame = webView.scrollView.frame
    if (webView.scrollView.contentSize != CGSize(width: 0,height: 0)){
    webView.scrollView.frame = CGRectMake(webViewFrame.origin.x, webViewFrame.origin.y, webView.scrollView.contentSize.width, webView.scrollView.contentSize.height)

    UIGraphicsBeginImageContextWithOptions(webView.scrollView.contentSize, webView.scrollView.opaque, 0)
    webView.scrollView.layer.renderInContext(UIGraphicsGetCurrentContext())
     var image:UIImage = UIGraphicsGetImageFromCurrentImageContext()
     UIGraphicsEndImageContext()

     webView.scrollView.frame = webViewFrame         
     return image
    }

    return nil
 }
shrutim
  • 1,058
  • 2
  • 12
  • 21
  • Can you link the page you're trying to load? – chedabob May 17 '15 at 20:29
  • http://en.wikipedia.org/wiki/Tom on iPhone 6 simulator XCode. Still waiting for my device to be provisioned – shrutim May 21 '15 at 17:22
  • I worked around by taking screenshot when user navigates away from the web page instead of didfinishNavigation. It is not a perfect solution, but I never really was able to get the screenshot at didFinishNavigation (or) when the KVO reported loading was done. – shrutim Apr 03 '16 at 01:04

7 Answers7

39

For those still looking for an answer to this, the marked answer is BS, he just forced his way into getting it accepted.

Using property,

"loading"

and

webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!)

both do the same thing, indicate if the main resource is loaded.

Now, that does not mean the entire webpage/website is loaded, because it really depends on the implementation of the website. If it needs to load scripts and resources (images, fonts etc) to make itself visible, you'll still see nothing after the navigation is completed, because the network calls made by the website are not tracked by the webview, only the navigation is tracked, so it wouldn't really know when the website loaded completely.

rae1
  • 6,066
  • 4
  • 27
  • 48
Santhosh R
  • 1,518
  • 2
  • 10
  • 14
  • 1
    And is there a way to accomplish a tracking of resource-loading (e.g. images)? – TMob Nov 24 '17 at 12:23
  • 3
    You can let the web app call your javascript messageHandler to let the iOS app know when a particular resource is loaded. However if its not your web app or not have access to its code, then its not quite possible. You can still try hacking into the js and find ways to observe certain things (like if theres a function thats only defined after an image is loaded, you can check if that exists at a given time which will tell you that an image is loaded) But yeah there's no straight forward way of tracking them. – Santhosh R Nov 27 '17 at 21:52
  • 2
    This should be marked as the correct solution - resource loading is the thing that catches everyone out. The loading flag merely means the html has loaded. It does not mean all layout, images and scripts are loaded. On simple pages this flag gives a false positive. JS callback when "enough" of the page has loaded is the only correct way to do this. – Alexp Feb 07 '18 at 15:12
  • This is the correct solution. "IsLoading" and "ContentSize" can't be relied on to determine when a page has fully rendered. – user3335999 Aug 03 '20 at 19:05
13

WKWebView doesn't use delegation to let you know when content loading is complete (that's why you can't find any delegate method that suits your purpose). The way to know whether a WKWebView is still loading is to use KVO (key-value observing) to watch its loading property. In this way, you receive a notification when loading changes from true to false.

Here's a looping animated gif showing what happens when I test this. I load a web view and respond to its loading property through KVO to take a snapshot. The upper view is the web view; the lower (squashed) view is the snapshot. As you can see, the snapshot does capture the loaded content:

enter image description here

[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    if (self->_webKitView.isLoading == true) {
        NSLog(@"Still loading...");
    }else {
        NSLog(@"Finished loading...");
        [timer invalidate];
        dispatch_async(dispatch_get_main_queue(), ^{
            [self->_activityIndicator stopAnimating];
        });
    }
}];
Krishna Raj Salim
  • 7,331
  • 5
  • 34
  • 66
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 1
    Actual example code here: https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/bk2ch11p552webkit/ch24p825webview/WebViewController.swift Notice how I put up an activity indicator when the page starts loading and use KVO to take it down again when loading has completed. – matt May 17 '15 at 20:56
  • 2
    I tried the KVO method. The "loading" key is changed to false even before the website is loaded. So this didn't work for me – shrutim May 21 '15 at 04:39
  • Tried using estimated progress key using KVO, but even this property is set to 1 before rendering happens – shrutim May 21 '15 at 06:01
  • I created the KVO literally copy pasting your code from the github link you posted and just added in the call to take screenshot when the loading is false (or) estimated progress = 1. In either cases the call is made before rendering happens – shrutim May 21 '15 at 16:48
  • Added the screenshot code to the question. Please take a look – shrutim May 21 '15 at 22:39
  • From the gif it seems like you are loading a local file. I dont know if that is a relevant example. As mentioned the questions comments, when I am loading a Wiki page on iPhone 6 simulator, the didFinishNavigation is called before the content is loaded for WKWebView. – shrutim May 21 '15 at 23:29
  • you sure did answer the question. It would be great if you could share the code. I did mark your response as the answer – shrutim May 22 '15 at 01:03
  • 1
    Check out http://stackoverflow.com/questions/32660051/wkwebview-notification-when-view-is-actually-shown/32660695#32660695 – peacer212 Sep 18 '15 at 22:34
  • 11
    In my tests, observing `loading` gives me the same result as using the `WKNavigationDelegate` delegate callback `webView:didFinishNavigation`. – abc123 Oct 16 '15 at 19:17
  • 'loading' is working, is there any key observer is available when finish load the url. – Vineesh TP Mar 26 '18 at 04:29
8

Here is how I solved it:

class Myweb: WKWebView {

    func setupWebView(link: String) {
        let url = NSURL(string: link)
        let request = NSURLRequest(URL: url!)
        loadRequest(request)
        addObserver(self, forKeyPath: "loading", options: .New, context: nil)
    }

    override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
        guard let _ = object as? WKWebView else { return }
        guard let keyPath = keyPath else { return }
        guard let change = change else { return }
        switch keyPath {
        case "loading":
            if let val = change[NSKeyValueChangeNewKey] as? Bool {
                if val {
                } else {
                    print(self.loading)
                    //do something!
                }
            }
        default:break
        }
    }

    deinit {
        removeObserver(self, forKeyPath: "loading")
    }
}

Update Swift 3.1

override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {

    guard let _ = object as? WKWebView else { return }
    guard let keyPath = keyPath else { return }
    guard let change = change else { return }

    switch keyPath {
    case "loading":
        if let val = change[NSKeyValueChangeKey.newKey] as? Bool {
            //do something!
        }
    default:
        break
    }
}
Stephen Chen
  • 3,027
  • 2
  • 27
  • 39
Esqarrouth
  • 38,543
  • 21
  • 161
  • 168
  • "loading" property does not insure web page is loaded. Tested on devices and loading return false before page is actually loaded in many cases. – Borut Tomazin Apr 07 '16 at 07:16
  • 'loading' is working, is there any key observer is available when finish load the url. – Vineesh TP Mar 26 '18 at 04:31
8

Lots of hand waiving in here, for incomplete solutions. The observer on "loading" is not reliable because when it's switched to NO, the layout hasn't happened yet and you can't get an accurate reading on page size.

The injection of JS into the view to report page size can also be problematic depending on actual page content.

The WKWebView uses, obviously, a scroller (a "WKWebScroller" subclass to be exact). So, your best bet is to monitor that scroller's contentSize.

    - (void) viewDidLoad {
    //...super etc
    [self.webKitView.scrollView addObserver: self
                                 forKeyPath: @"contentSize"
                                    options: NSKeyValueObservingOptionNew
                                    context: nil];
    }

    - (void) dealloc
    {
        // remove observer
        [self.webKitView.scrollView removeObserver: self
                                        forKeyPath: @"contentSize"];
    }

    - (void) observeValueForKeyPath: (NSString*) keyPath
                           ofObject: (id) object
                             change: (NSDictionary<NSKeyValueChangeKey,id>*) change
                            context: (void*) context
    {
        if ([keyPath isEqualToString: @"contentSize"])
        {
            UIScrollView*   scroller    =   (id) object;
            CGSize          scrollFrame =   scroller.contentSize;

            NSLog(@"scrollFrame = {%@,%@}",
                  @(scrollFrame.width), @(scrollFrame.height));
        }
    }

Watch out for the contentSize: it gets triggered A LOT. If your webView is embedded into another scrolling area (like if you're using it as a form element) then when you scroll your entire view, that subview webView will trigger the changes for the same values. So, make sure you dont resize needlessly or cause needless refreshes.

This solution tested on iOS 12 on iPad Air 2 sim off High Sierra XCode 10.

  • Does anyone now how to do this for Mac? The scroll view property is only accessible on iOS. – Berry Blue Nov 28 '19 at 01:35
  • This is the best I read during the last days regarding checking for finished loading/rendering of WKWebView pages. I confirm that the `loading` observer is not reliable. Same for the `didFinish navigation:` method. Both fire before the page is completely loaded/rendered. `WKWebView.scrollView.contentSize` observer does the job! Many thanks. – geohei Sep 28 '22 at 15:59
1

It's not a good choice to check if the page content loaded from swift or objective-c especially for very complex page with many dynamic content.

A better way to inform you ios code from webpage's javascript. This make it very flexible and effective, you can notify ios code when a dom or the page is loaded.

You can check my post here for further info.

Community
  • 1
  • 1
LF00
  • 27,015
  • 29
  • 156
  • 295
1

Most of these answers likely won't give you the results you're looking for.

Let the html document tell you when it's loaded.

Here is how it is done.

script message handler delegate

@interface MyClass : UIView <WKScriptMessageHandler>

Initialize the WKView to handle whatever event you'd like (e.g. window.load)

WKWebView* webView = yourWebView;

NSString* jScript = @"window.addEventListener('load', function () { window.webkit.messageHandlers.loadEvent.postMessage('loaded');})";

WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jScript injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];

[webView.configuration.userContentController addScriptMessageHandler:self name:@"loadEvent"];
[webView.configuration.userContentController addUserScript:wkUScript];

Handle the delegate message.

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    NSString* name = message.name;
    if([name compare:@"loadEvent"] == 0)
    {
    }
}
user3335999
  • 392
  • 1
  • 2
  • 17
0

You can inject JavaScript into the web view to either wait for onDOMContentLoaded or check the document.readyState state. I have an app that has been doing this for years, and it was the only reliable way to wait for the DOM to be populated. If you need all the images and other resources to be loaded, then you need to wait for the load event using JavaScript. Here are some docs:

https://developer.mozilla.org/en-US/docs/Web/API/Window/DOMContentLoaded_event https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState

Jordan Wood
  • 2,727
  • 3
  • 12
  • 17