Startry
Startry

Reputation: 584

How to capture a full page screenshot of WKWebview?

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:

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?

Upvotes: 2

Views: 13839

Answers (7)

Alterecho
Alterecho

Reputation: 715

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

Upvotes: 4

Philipp Otto
Philipp Otto

Reputation: 4111

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]
                }
            })
        })
    })
}

Upvotes: 0

Armağan Özdemir
Armağan Özdemir

Reputation: 11

- 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

Upvotes: 1

Maverick
Maverick

Reputation: 3259

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)

Upvotes: 9

RKS
RKS

Reputation: 1423

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)
        }
    }
}

Upvotes: 2

user89862
user89862

Reputation:

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
}

Upvotes: 4

Jason Honcheung Wong
Jason Honcheung Wong

Reputation: 65

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

Upvotes: -3

Related Questions