您好,登錄后才能下訂單哦!
前言
WebView就是一個內嵌瀏覽器控件,在iOS中主要有兩種WebView:UIWebView和WKWebView,UIWebView是iOS2之后開始使用,WKWebView是在iOS8開始使用,WKWebView將逐步取代笨重的UIWebView。
由于項目需要,新近實現了一個長截圖庫 SnapshotKit。其中,需要支持 UIWebView、WKWebView 組件生成長截圖。為了實現這個特性,查閱了很多資料,同時也做了不同的新奇思路嘗試,最終實現了一個新的、取巧的技術方案。
以下主要總結了在“WebView生成長截圖”需求方面,“網上已有方案”和“我的全新方案”的各自實現要點和優缺點。
WebView生成長截圖的已有方案
根據 Google 所搜索到的資料,目前iOS WebView生成長截圖的方案主要有2種:
下面將會簡述方案一和方案二的具體實現。
方案一:修改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文件上,借助這個特性,新方案的設計如下:
新方案的核心代碼如下:
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 截圖示例查看具體截圖效果。
以上三個方案,總的來說,解決了部分場景的需求,但都不夠完美,仍需做進一步的優化。
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。