preface

Due to the needs of the project, recently implemented a long screenshot library SnapshotKit. Among them, UIWebView and WKWebView components need to be supported to generate growth screenshots. In order to achieve this feature, consult a lot of information, but also do different novel ideas to try, and finally achieve a new, cheating technical scheme.

The following mainly summarizes the main points and advantages and disadvantages of the implementation of “online existing scheme” and “my new scheme” in terms of the demand of “WebView growth screenshot”.

WebView growth screenshots of the existing scheme

According to the information searched by Google, there are two main schemes for iOS WebView growth screenshots at present:

  • Solution 1: Modify Frame and screenshot components
  • Scheme 2: Page capture component content, combined with the growth diagram

The specific implementations of plan 1 and Plan 2 are briefly described below.

Solution 1: Modify Frame and screenshot components

The main points of scheme 1 are: modify the frameSize of WebView. scrollView to contentSize, and then take screenshots of the whole WebView. scrollView.

However, this solution only works with the UIWebView component because it loads all the content of the web page at once. The WKWebView component, in order to save memory, loads only the visible portion of the web content — similar to the UITableView component. After modifying the frameSize of WebView. scrollView, the screenshot operation is performed immediately. At this time, WKWebView has not loaded the content of the web page, resulting in the long screenshot generated is blank.

The core code of scheme 1 is as follows:

extension UIScrollView {
   public func takeSnapshotOfFullContent(a) -> UIImage? {
        let originalFrame = self.frame
        let originalOffset = self.contentOffset

        self.frame = CGRect.init(origin: originalFrame.origin, size: self.contentSize)
        self.contentOffset = .zero

        let backgroundColor = self.backgroundColor ?? UIColor.white

        UIGraphicsBeginImageContextWithOptions(self.bounds.size, true.0)

        guard let context = UIGraphicsGetCurrentContext(a)else {
            return nil
        }
        context.setFillColor(backgroundColor.cgColor)
        context.setStrokeColor(backgroundColor.cgColor)

        self.drawHierarchy(in: self.bounds, afterScreenUpdates: true)
        let image = UIGraphicsGetImageFromCurrentImageContext(a)UIGraphicsEndImageContext(a)self.frame = originalFrame
        self.contentOffset = originalOffset

        return image
    }
}
Copy the code

Test code:

// example code
 private func takeSnapshotOfUIWebView(a) {
    let image = self.webView.scrollView.takeSnapshotOfFullContent()
   / / image processing
}    
Copy the code

Scheme 2: Page capture component content, combined with the growth diagram

The main points of scheme 2 are: paging scrolling WebView component content, and then generate paging screenshots, and finally synthesize all paging screenshots into a long graph.

This scheme applies to UIWebView components and WKWebView components.

The core code of scheme 2 is as follows:

extension UIScrollView {
    public func takeScreenshotOfFullContent(_ completion: @escaping ((UIImage?) -> Void)) {
        // Page draw content to ImageContext
        let originalOffset = self.contentOffset

        Height 
        var pageNum = 1
        if self.contentSize.height > self.bounds.height {
            pageNum = Int(floorf(Float(self.contentSize.height / self.bounds.height)))
        }

        let backgroundColor = self.backgroundColor ?? UIColor.white

        UIGraphicsBeginImageContextWithOptions(self.contentSize, true.0)

        guard let context = UIGraphicsGetCurrentContext(a)else {
            completion(nil)
            return
        }
        context.setFillColor(backgroundColor.cgColor)
        context.setStrokeColor(backgroundColor.cgColor)

        self.drawScreenshotOfPageContent(0, maxIndex: pageNum) {
            let image = UIGraphicsGetImageFromCurrentImageContext(a)UIGraphicsEndImageContext(a)self.contentOffset = originalOffset
            completion(image)
        }
    }

    fileprivate func drawScreenshotOfPageContent(_ index: Int, maxIndex: Int, completion: @escaping (a) -> Void) {

        self.setContentOffset(CGPoint(x: 0, y: CGFloat(index) * self.frame.size.height), animated: false)
        let pageFrame = CGRect(x: 0, y: CGFloat(index) * self.frame.size.height, width: self.bounds.size.width, height: self.bounds.size.height)

        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) {
            self.drawHierarchy(in: pageFrame, afterScreenUpdates: true)

            if index < maxIndex {
                self.drawScreenshotOfPageContent(index + 1, maxIndex: maxIndex, completion: completion)
            }else{
                completion()
            }
        }
    }
}
Copy the code

Test code:

// example code
private func takeSnapshotOfUIWebView(a) {
    self.uiWebView.scrollView.takeScreenshotOfFullContent { (image) in
        / / image processing}}private func takeSnapshotOfWKWebView(a) {
    self.wkWebView.scrollView.takeScreenshotOfFullContent { (image) in
        / / image processing}}Copy the code

WebView generates a new solution for growing screenshots

Besides plan 1 and plan 2, are there any new plans?

The answer is yes plus yes and yes.

The key to this new solution: iOS WebView printing.

IOS supports printing WebView content to PDF files. With this feature, the design of the new solution is as follows:

  1. Print the contents of the WebView component to a SINGLE PDF page

  2. Convert PDF to picture

The core code of the new solution is as follows:

import UIKit
import WebKit

/// WebViewPrintPageRenderer: use to print the full content of webview into one image
internal final class WebViewPrintPageRenderer: UIPrintPageRenderer {

    private var formatter: UIPrintFormatter

    private var contentSize: CGSize

    // Generate the PrintPageRenderer instance
    ///
    /// - Parameters:
    /// -formatter: WebView viewPrintFormatter
    /// - contentSize: WebView contentSize
    required init(formatter: UIPrintFormatter, contentSize: CGSize) {
        self.formatter = formatter
        self.contentSize = contentSize
        super.init(a)self.addPrintFormatter(formatter, startingAtPageAt: 0)}override var paperRect: CGRect {
        return CGRect.init(origin: .zero, size: contentSize)
    }

    override var printableRect: CGRect {
        return CGRect.init(origin: .zero, size: contentSize)
    }

    private func printContentToPDFPage(a) -> CGPDFPage? {
        let data = NSMutableData(a)UIGraphicsBeginPDFContextToData(data, self.paperRect, nil)
        self.prepare(forDrawingPages: NSMakeRange(0.1))
        let bounds = UIGraphicsGetPDFContextBounds(a)UIGraphicsBeginPDFPage(a)self.drawPage(at: 0.in: bounds)
        UIGraphicsEndPDFContext(a)let cfData = data as CFData
        guard let provider = CGDataProvider.init(data: cfData) else {
            return nil
        }
        let pdfDocument = CGPDFDocument.init(provider)
        letpdfPage = pdfDocument? .page(at:1)

        return pdfPage
    }

    private func covertPDFPageToImage(_ pdfPage: CGPDFPage) -> UIImage? {
        let pageRect = pdfPage.getBoxRect(.trimBox)
        let contentSize = CGSize.init(width: floor(pageRect.size.width), height: floor(pageRect.size.height))

        / / usually you want UIGraphicsBeginImageContextWithOptions last parameter to be 0.0 as this will us the device 's scale
        UIGraphicsBeginImageContextWithOptions(contentSize, true.2.0)
        guard let context = UIGraphicsGetCurrentContext(a)else {
            return nil
        }

        context.setFillColor(UIColor.white.cgColor)
        context.setStrokeColor(UIColor.white.cgColor)
        context.fill(pageRect)

        context.saveGState()
        context.translateBy(x: 0, y: contentSize.height)
        context.scaleBy(x: 1.0, y: -1.0)

        context.interpolationQuality = .low
        context.setRenderingIntent(.defaultIntent)
        context.drawPDFPage(pdfPage)
        context.restoreGState()

        let image = UIGraphicsGetImageFromCurrentImageContext(a)UIGraphicsEndImageContext(a)return image
    }

    /// print the full content of webview into one image
    ///
    /// - Important: if the size of content is very large, then the size of image will be also very large
    /// - Returns: UIImage?
    internal func printContentToImage(a) -> UIImage? {
        guard let pdfPage = self.printContentToPDFPage() else {
            return nil
        }

        let image = self.covertPDFPageToImage(pdfPage)
        return image
    }
}

extension UIWebView {
    public func takeScreenshotOfFullContent(_ completion: @escaping ((UIImage?) -> Void)) {
        self.scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: false)
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) {
            let renderer = WebViewPrintPageRenderer.init(formatter: self.viewPrintFormatter(), contentSize: self.scrollView.contentSize)
            let image = renderer.printContentToImage()
            completion(image)
        }
    }
}

extension WKWebView {
    public func takeScreenshotOfFullContent(_ completion: @escaping ((UIImage?) -> Void)) {
        self.scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: false)
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) {
            let renderer = WebViewPrintPageRenderer.init(formatter: self.viewPrintFormatter(), contentSize: self.scrollView.contentSize)
            let image = renderer.printContentToImage()
            completion(image)
        }
    }
}
Copy the code

WebViewPrintPageRenderer, the core class of the solution, is responsible for printing WebView component content to a PDF and then converting the PDF to an image.

UIWebView and WKWebView implement corresponding extensions.

Test code:

// example code
private func takeSnapshotOfUIWebView(a) {
    self.uiWebView.scrollView.takeScreenshotOfFullContent { (image) in
        / / image processing}}private func takeSnapshotOfWKWebView(a) {
    self.wkWebView.scrollView.takeScreenshotOfFullContent { (image) in
        / / image processing}}Copy the code

Comparison of advantages and disadvantages of three technical schemes

So, what are the advantages and disadvantages of these three technical solutions, and what scenarios are they applicable to?

  • Plan 1: Applicable onlyUIWebView; If the page has a lot of content, it will take up too much memory when creating screenshots. Therefore, the scheme is only suitable without supportWKWebView, and the web content will not be too many scenes.
  • Plan two: ApplicationUIWebViewWKWebViewAnd it is particularly suitableWKWebView. Due to the use of paging screenshot generation mechanism, effectively reduce memory consumption. However, there is a problem with this solution: if the web page existsposition: fixedElement (such as a fixed navigation bar at the head of a web page) that appears repeatedly on the generated long graph.
  • Plan three: ApplicationUIWebViewWKWebView. The most important step — “print WebView content to PDF” is implemented by iOS system, so the performance of this solution can be guaranteed in theory. However, there is a problem with this solution: when the web content is printed to a PDF, the iOS system gets itcontentSizeMore realistic than WebViewcontentSizeLarge, resulting in a slightly different image near the bottom of the content. Specific can download run my long cut gallerySnapshotKitThe Demo, through whichUIWebViewWKWebViewScreenshot Example View the screenshot effect.

The above three schemes, on the whole, have solved the requirements of some scenes, but they are not perfect enough and still need to be further optimized.