iOS使用WebView生成长截图的第3种解决方案
前言
webview就是一个内嵌浏览器控件,在ios中主要有两种webview:uiwebview和wkwebview,uiwebview是ios2之后开始使用,wkwebview是在ios8开始使用,wkwebview将逐步取代笨重的uiwebview。
由于项目需要,新近实现了一个长截图库 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 截图示例查看具体截图效果。
以上三个方案,总的来说,解决了部分场景的需求,但都不够完美,仍需做进一步的优化。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。