1

I can capture all content screenshot of UIWebView by adjust frame of UIScrollView of UIWebView. However, I can't capture all content screenshot of WKWebView by the same method.

The method I used for capture UIWebView as follow:

  1. backup frame and superview of webview.scrollView
  2. create a new container view as webview.scrollView's superview
  3. adjust frame of new container view and webview.scrollView. frame is same to webview.scrollView.contentSize
  4. draw by renderInContext or drawViewHierarchyInRect

However, this method will capture a white screenshot of WKWebview. It doesn't work!

I had print all level of WKWebView, then I found a UIView(WKContentView)'s size is same to contentView, you can found this view by this level:

  • WKWebView
    • WKScrollView
      • WKContentView(size is same to contentView)

I also had try to capture by WKContentView, then I found only visible view could be captured.

Anyway, Anyone could tell me how to capture a full page content screenshot of WKWebView?

Nikunj
  • 655
  • 3
  • 13
  • 25
Startry
  • 574
  • 1
  • 4
  • 17
  • I had capture WKWebView by create many screenshots and compose to a big UIImage. However, the `display: absolute;` div will be draw many times in this case. – Startry Feb 19 '16 at 03:09
  • I write a lib to capture WKWebView: https://github.com/startry/SwViewCapture, but still has `display: absolute` div repeat problem. – Startry Feb 19 '16 at 09:03
  • `display: absolute;` -> `position: absolute;` – Startry Feb 19 '16 at 09:47

7 Answers7

7

Please refer to this answer

iOS 11.0 and above, Apple has provided following API to capture snapshot of WKWebView.

@available(iOS 11.0, *)
    open func takeSnapshot(with snapshotConfiguration: WKSnapshotConfiguration?, completionHandler: @escaping (UIImage?, Error?) -> Swift.Void)
Maverick
  • 3,209
  • 1
  • 34
  • 40
4

Swift 3, Xcode 8

func takeScreenshot() -> UIImage? { 
    let currentSize = webView.frame.size
    let currentOffset = webView.scrollView.contentOffset

    webView.frame.size = webView.scrollView.contentSize
    webView.scrollView.setContentOffset(CGPoint.zero, animated: false)

    let rect = CGRect(x: 0, y: 0, width: webView.bounds.size.width, height: webView.bounds.size.height)
    UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.main.scale)
    webView.drawHierarchy(in: rect, afterScreenUpdates: true)
    let image = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()

    webView.frame.size = currentSize
    webView.scrollView.setContentOffset(currentOffset, animated: false)

    return image
}
  • it's not taking complete wkwebview screens shot – Yatendra Jun 28 '18 at 10:10
  • @Yatendra, this is the way to go, but you have to do it in two passes, first pass detect the real size using the scrollview content size, update the WebView frame to be of that size and refresh or reload or whatever you are using to load the webview, now it will render, because if the size exceeds certain size it will be cropped, save the debug view of the webview, and open it on a image editor, you will see that the cropping is always a fixed size, my case was 1024 points in height, everything below that was cropped/solid color background. – Juan Boero Sep 13 '19 at 04:36
3

The key here is to let capture after webkit be allowed time to render after it is given a new frame. I tried didFinishLoading calback and WKWebView's wkWebView.takeSnapshot, but did not work. So I introduced an a delay for the screenshot:

    func createImage(webView: WKWebView, completion: @escaping (UIImage?) -> ()) {
    
    // save the original size to restore
    let originalFrame = webView.frame
    let originalConstraints = webView.constraints
    let originalScrollViewOffset = webView.scrollView.contentOffset
    
    let newSize = webView.scrollView.contentSize
    
    // remove any constraints for the web view, and set the size
    // to be size of the content size (will be restored later)
    webView.removeConstraints(originalConstraints)
    webView.translatesAutoresizingMaskIntoConstraints = true
    webView.frame = CGRect(origin: .zero, size: newSize)
    webView.scrollView.contentOffset = .zero
    
    // wait for a while for the webview to render in the newly set frame
    DispatchQueue.main.asyncAfter(deadline: .now() + 4.0) {
        defer {
            UIGraphicsEndImageContext()
        }
        UIGraphicsBeginImageContextWithOptions(newSize, false, 0)
        if let context = UIGraphicsGetCurrentContext() {
            // render the scroll view's layer
            webView.scrollView.layer.render(in: context)
            
            // restore the original state
            webView.frame = originalFrame
            webView.translatesAutoresizingMaskIntoConstraints = false
            webView.addConstraints(originalConstraints)
            webView.scrollView.contentOffset = originalScrollViewOffset
            
            if let image = UIGraphicsGetImageFromCurrentImageContext() {
                completion(image)
            } else {
                completion(nil)
            }
        }
    }
}

Result:

enter image description here

Alterecho
  • 665
  • 10
  • 25
2

Swift 3, Xcode 9: I created an extension to achieve this. Hope this helps you.

extension WKWebView{

     private func stageWebViewForScreenshot() {
        let _scrollView = self.scrollView
        let pageSize = _scrollView.contentSize;
        let currentOffset = _scrollView.contentOffset
        let horizontalLimit = CGFloat(ceil(pageSize.width/_scrollView.frame.size.width))
        let verticalLimit = CGFloat(ceil(pageSize.height/_scrollView.frame.size.height))

         for i in stride(from: 0, to: verticalLimit, by: 1.0) {
            for j in stride(from: 0, to: horizontalLimit, by: 1.0) {
                _scrollView.scrollRectToVisible(CGRect(x: _scrollView.frame.size.width * j, y: _scrollView.frame.size.height * i, width: _scrollView.frame.size.width, height: _scrollView.frame.size.height), animated: true)
                 RunLoop.main.run(until: Date.init(timeIntervalSinceNow: 1.0))
            }
        }
        _scrollView.setContentOffset(currentOffset, animated: false)
    }

     func fullLengthScreenshot(_ completionBlock: ((UIImage) -> Void)?) {
        // First stage the web view so that all resources are downloaded.
         stageWebViewForScreenshot()

        let _scrollView = self.scrollView

        // Save the current bounds
        let tmp = self.bounds
        let tmpFrame = self.frame
         let currentOffset = _scrollView.contentOffset

        // Leave main thread alone for some time to let WKWebview render its contents / run its JS to load stuffs.
         mainDispatchAfter(2.0) {
            // Re evaluate the size of the webview
            let pageSize = _scrollView.contentSize
             UIGraphicsBeginImageContext(pageSize)

            self.bounds = CGRect(x: self.bounds.origin.x, y: self.bounds.origin.y, width: pageSize.width, height: pageSize.height)
            self.frame = CGRect(x: self.frame.origin.x, y: self.frame.origin.y, width: pageSize.width, height: pageSize.height)

            // Wait few seconds until the resources are loaded
            RunLoop.main.run(until: Date.init(timeIntervalSinceNow: 0.5))

             self.layer.render(in: UIGraphicsGetCurrentContext()!)
            let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
            UIGraphicsEndImageContext()

            // reset Frame of view to origin
            self.bounds = tmp
            self.frame = tmpFrame
            _scrollView.setContentOffset(currentOffset, animated: false)

             completionBlock?(image)
        }
    }
}
RKS
  • 1,333
  • 2
  • 12
  • 12
1

- xcode 10 & swift 4.2 -

Use this;

// this is the button which takes the screenshot

   @IBAction func snapShot(_ sender: Any) {

       captureScreenshot()
    }

// this is the function which is the handle screenshot functionality.

func captureScreenshot(){
    let layer = UIApplication.shared.keyWindow!.layer
    let scale = UIScreen.main.scale
    // Creates UIImage of same size as view
    UIGraphicsBeginImageContextWithOptions(layer.frame.size, false, scale);
    layer.render(in: UIGraphicsGetCurrentContext()!)
    let screenshot = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    // THIS IS TO SAVE SCREENSHOT TO PHOTOS
    UIImageWriteToSavedPhotosAlbum(screenshot!, nil, nil, nil)
}

and you must add these keys in your info.plist;

 key value = "Privacy - Photo Library Additions Usage Description" ,
 key value = Privacy - Photo Library Usage Description
Bhavesh Nayi
  • 3,626
  • 1
  • 27
  • 42
0

Here is an iOS 11+ example for snapshotting the WKWebView without the need of any delays to render the content.

The key is to make sure that you dont receive a blank snapshot. Initially we had issues with the output image being blank and having to introduce a delay to have the web content in the output image. I see the solution with the delay in many posts and I would recommend to not use it because it is an unnecessarily unreliable and unperformant solution. The important solution for us was to set the rect of WKSnapshotConfiguration and to use the JavaScript functions to wait for the rendering and also receiving the correct width and height.

Setting up the WKWebView:

// Important: You can set a width here which will also be reflected in the width of the snapshot later
let webView = WKWebView(frame: .zero)

webView.configuration.dataDetectorTypes = []
webView.navigationDelegate = self

webView.scrollView.contentInsetAdjustmentBehavior = .never

webView.loadHTMLString(htmlString, baseURL: Bundle.main.resourceURL)

Capturing the snapshot:

func webView(
    _ webView: WKWebView,
    didFinish navigation: WKNavigation!
) {
    // Wait for the page to be rendered completely and get the final size from javascript
    webView.evaluateJavaScript("document.readyState", completionHandler: { [weak self] (readyState, readyStateError) in
        webView.evaluateJavaScript("document.documentElement.scrollWidth", completionHandler: {(contentWidth, widthError) in
            webView.evaluateJavaScript("document.documentElement.scrollHeight", completionHandler: { (contentHeight, heightError) in
                guard readyState != nil,
                    let contentHeight = contentHeight as? CGFloat,
                    let contentWidth = contentWidth as? CGFloat else {
                    [Potential error handling...]
                    return
                }

                let rect = CGRect(
                    x: 0,
                    y: 0,
                    width: contentWidth,
                    height: contentHeight
                )

                let configuration = WKSnapshotConfiguration()

                configuration.rect = rect

                if #available(iOS 13.0, *) {
                    configuration.afterScreenUpdates =  true
                }

                webView.takeSnapshot(with: configuration) { (snapshotImage, error) in
                    guard let snapshotImage = snapshotImage else {
                        [Potential error handling...]
                    }

                    [Do something with the image]
                }
            })
        })
    })
}
Philipp Otto
  • 4,061
  • 2
  • 32
  • 49
-3

UPDATE: Not sure why Jason is sticking this answer all over the net. IT DOES NOT solve the problem of HOW to Screenshot the Full WKWebView content... including that off the screen.

Try this:

func snapshot() -> UIImage {
        UIGraphicsBeginImageContextWithOptions(self.webView.bounds.size, true, 0);
        self.webView.drawViewHierarchyInRect(self.webView.bounds, afterScreenUpdates: true);
        let snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();

        UIImageWriteToSavedPhotosAlbum(snapshotImage, nil, nil, nil)

        return snapshotImage
    }

The image will automatically save into the iOS Camera Roll (by UIImageWriteToSavedPhotosAlbum()).

Cliff Ribaudo
  • 8,932
  • 2
  • 55
  • 78