前言
由於專案需要,新近實現了一個長截相簿 SnapshotKit。其中,需要支援 UIWebView
、WKWebView
元件生成長截圖。為了實現這個特性,查閱了很多資料,同時也做了不同的新奇思路嘗試,最終實現了一個新的、取巧的技術方案。
以下主要總結了在“WebView生成長截圖”需求方面,“網上已有方案”和“我的全新方案”的各自實現要點和優缺點。
WebView生成長截圖的已有方案
根據 Google 所搜尋到的資料,目前iOS WebView生成長截圖的方案主要有2種:
- 方案一:修改Frame,截圖元件
- 方案二:分頁截圖元件內容,合成長圖
下面將會簡述方案一和方案二的具體實現。
方案一:修改Frame,截圖元件
方案一的實現要點在於:修改 webView.scrollView
的 frameSize
為 contentSize
,然後對整個 webView.scrollView
進行截圖。
不過,這個方案只適用 UIWebView
元件,因為其是一次性載入網頁所有的內容。而 WKWebView
元件,為了節省記憶體,載入網頁內容時,只載入可視部分——這一點類似 UITableView
元件。在修改webView.scrollView
的 frameSize
後,立即執行了截圖操作, 這時候,WKWebView
由於還沒把網頁的內容載入出來,導致生成的長截圖是空白的。
方案一核心程式碼如下:
extension UIScrollView {
public func takeSnapshotOfFullContent() -> 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() else {
return nil
}
context.setFillColor(backgroundColor.cgColor)
context.setStrokeColor(backgroundColor.cgColor)
self.drawHierarchy(in: self.bounds, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.frame = originalFrame
self.contentOffset = originalOffset
return image
}
}
複製程式碼
測試程式碼:
// example code
private func takeSnapshotOfUIWebView() {
let image = self.webView.scrollView.takeSnapshotOfFullContent()
// 處理image
}
複製程式碼
方案二:分頁截圖元件內容,合成長圖
方案二的實現要點在於:分頁滾動WebView元件的內容,然後生成分頁截圖,最後把所有分頁截圖合成一張長圖。
這個方案適用於 UIWebView
元件和 WKWebView
元件。
方案二核心程式碼如下:
extension UIScrollView {
public func takeScreenshotOfFullContent(_ completion: @escaping ((UIImage?) -> Void)) {
// 分頁繪製內容到ImageContext
let originalOffset = self.contentOffset
// 當contentSize.height<bounds.height時,保證至少有1頁的內容繪製
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() else {
completion(nil)
return
}
context.setFillColor(backgroundColor.cgColor)
context.setStrokeColor(backgroundColor.cgColor)
self.drawScreenshotOfPageContent(0, maxIndex: pageNum) {
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.contentOffset = originalOffset
completion(image)
}
}
fileprivate func drawScreenshotOfPageContent(_ index: Int, maxIndex: Int, completion: @escaping () -> 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()
}
}
}
}
複製程式碼
測試程式碼:
// example code
private func takeSnapshotOfUIWebView() {
self.uiWebView.scrollView.takeScreenshotOfFullContent { (image) in
// 處理image
}
}
private func takeSnapshotOfWKWebView() {
self.wkWebView.scrollView.takeScreenshotOfFullContent { (image) in
// 處理image
}
}
複製程式碼
WebView生成長截圖的新方案
除了方案一和方案二,還有新方案嗎?
答案是肯定加確定以及一定的。
這個新方案的要點在於:iOS系統的WebView列印功能。
iOS系統支援把WebView的內容列印到PDF檔案上,藉助這個特性,新方案的設計如下:
-
把 WebView元件的內容全部列印到一頁PDF上
-
把PDF轉換成圖片
新方案的核心程式碼如下:
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
/// 生成PrintPageRenderer例項
///
/// - Parameters:
/// - formatter: WebView的viewPrintFormatter
/// - contentSize: WebView的ContentSize
required init(formatter: UIPrintFormatter, contentSize: CGSize) {
self.formatter = formatter
self.contentSize = contentSize
super.init()
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() -> CGPDFPage? {
let data = NSMutableData()
UIGraphicsBeginPDFContextToData(data, self.paperRect, nil)
self.prepare(forDrawingPages: NSMakeRange(0, 1))
let bounds = UIGraphicsGetPDFContextBounds()
UIGraphicsBeginPDFPage()
self.drawPage(at: 0, in: bounds)
UIGraphicsEndPDFContext()
let cfData = data as CFData
guard let provider = CGDataProvider.init(data: cfData) else {
return nil
}
let pdfDocument = CGPDFDocument.init(provider)
let pdfPage = 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() 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()
UIGraphicsEndImageContext()
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() -> 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)
}
}
}
複製程式碼
WebViewPrintPageRenderer
是該方案的核心類,負責把 WebView元件
內容列印到PDF,然後把PDF轉換為圖片。
UIWebView
和 WKWebView
則實現對應的擴充套件。
測試程式碼:
// example code
private func takeSnapshotOfUIWebView() {
self.uiWebView.scrollView.takeScreenshotOfFullContent { (image) in
// 處理image
}
}
private func takeSnapshotOfWKWebView() {
self.wkWebView.scrollView.takeScreenshotOfFullContent { (image) in
// 處理image
}
}
複製程式碼
三種技術方案優劣對比
那麼,這三種技術方案各自存在什麼優缺點呢,適用什麼場景呢?
- 方案一:只適用
UIWebView
;若網頁內容很多,生成長截圖時,會佔用過多記憶體。 所以,該方案只適合不需要支援WKWebView
, 且網頁內容不會太多的場景。 - 方案二:適用
UIWebView
和WKWebView
,且特別適合WKWebView
。由於採用分頁生成截圖機制,有效減少記憶體佔用。不過,這個方案存在一個問題:若網頁存在position: fixed
的元素(如網頁頭部固定的導航欄),該元素會重複出現在生成的長圖上。 - 方案三:適用
UIWebView
和WKWebView
。其中最重要的一步——“把WebView內容列印到PDF” 是由iOS系統實現,所以該方案的效能在理論上是可以得到保障的。不過,這個方案存在一個問題:在把網頁內容列印到PDF時,iOS系統獲取的contentSize
比WebView的實際contentSize
要大,從而導致生成的圖片在靠近底部的內容部分和實際存在一點差異。具體可以下載執行我的長截相簿 SnapshotKit 的 Demo,通過其中的UIWebView
和WKWebView
截圖示例檢視具體截圖效果。
以上三個方案,總的來說,解決了部分場景的需求,但都不夠完美,仍需做進一步的優化。