iOS使用WebView生成长截图的第3种解决方案
前言
WebView就是一个内嵌浏览器控件,在iOS中主要有两种WebView:UIWebView和WKWebView,UIWebView是iOS2之后开始使用,WKWebView是在iOS8开始使用,WKWebView将逐步取代笨重的UIWebView。
由于项目需要,新近实现了一个长截图库SnapshotKit。其中,需要支持UIWebView、WKWebView组件生成长截图。为了实现这个特性,查阅了很多资料,同时也做了不同的新奇思路尝试,最终实现了一个新的、取巧的技术方案。
以下主要总结了在“WebView生成长截图”需求方面,“网上已有方案”和“我的全新方案”的各自实现要点和优缺点。
WebView生成长截图的已有方案
根据Google所搜索到的资料,目前iOSWebView生成长截图的方案主要有2种:
- 方案一:修改Frame,截图组件
- 方案二:分页截图组件内容,合成长图
下面将会简述方案一和方案二的具体实现。
方案一:修改Frame,截图组件
方案一的实现要点在于:修改webView.scrollView的frameSize 为contentSize,然后对整个webView.scrollView进行截图。
不过,这个方案只适用UIWebView组件,因为其是一次性加载网页所有的内容。而WKWebView组件,为了节省内存,加载网页内容时,只加载可视部分——这一点类似UITableView组件。在修改webView.scrollView的frameSize后,立即执行了截图操作,这时候,WKWebView由于还没把网页的内容加载出来,导致生成的长截图是空白的。
方案一核心代码如下:
extensionUIScrollView{
publicfunctakeSnapshotOfFullContent()->UIImage?{
letoriginalFrame=self.frame
letoriginalOffset=self.contentOffset
self.frame=CGRect.init(origin:originalFrame.origin,size:self.contentSize)
self.contentOffset=.zero
letbackgroundColor=self.backgroundColor??UIColor.white
UIGraphicsBeginImageContextWithOptions(self.bounds.size,true,0)
guardletcontext=UIGraphicsGetCurrentContext()else{
returnnil
}
context.setFillColor(backgroundColor.cgColor)
context.setStrokeColor(backgroundColor.cgColor)
self.drawHierarchy(in:self.bounds,afterScreenUpdates:true)
letimage=UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.frame=originalFrame
self.contentOffset=originalOffset
returnimage
}
}
测试代码:
//examplecode
privatefunctakeSnapshotOfUIWebView(){
letimage=self.webView.scrollView.takeSnapshotOfFullContent()
//处理image
}
方案二:分页截图组件内容,合成长图
方案二的实现要点在于:分页滚动WebView组件的内容,然后生成分页截图,最后把所有分页截图合成一张长图。
这个方案适用于UIWebView组件和WKWebView组件。
方案二核心代码如下:
extensionUIScrollView{
publicfunctakeScreenshotOfFullContent(_completion:@escaping((UIImage?)->Void)){
//分页绘制内容到ImageContext
letoriginalOffset=self.contentOffset
//当contentSize.heightself.bounds.height{
pageNum=Int(floorf(Float(self.contentSize.height/self.bounds.height)))
}
letbackgroundColor=self.backgroundColor??UIColor.white
UIGraphicsBeginImageContextWithOptions(self.contentSize,true,0)
guardletcontext=UIGraphicsGetCurrentContext()else{
completion(nil)
return
}
context.setFillColor(backgroundColor.cgColor)
context.setStrokeColor(backgroundColor.cgColor)
self.drawScreenshotOfPageContent(0,maxIndex:pageNum){
letimage=UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.contentOffset=originalOffset
completion(image)
}
}
fileprivatefuncdrawScreenshotOfPageContent(_index:Int,maxIndex:Int,completion:@escaping()->Void){
self.setContentOffset(CGPoint(x:0,y:CGFloat(index)*self.frame.size.height),animated:false)
letpageFrame=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)
ifindex
测试代码:
//examplecode
privatefunctakeSnapshotOfUIWebView(){
self.uiWebView.scrollView.takeScreenshotOfFullContent{(image)in
//处理image
}
}
privatefunctakeSnapshotOfWKWebView(){
self.wkWebView.scrollView.takeScreenshotOfFullContent{(image)in
//处理image
}
}
WebView生成长截图的新方案
除了方案一和方案二,还有新方案吗?
答案是肯定加确定以及一定的。
这个新方案的要点在于:iOS系统的WebView打印功能。
iOS系统支持把WebView的内容打印到PDF文件上,借助这个特性,新方案的设计如下:
- 把WebView组件的内容全部打印到一页PDF上
- 把PDF转换成图片
新方案的核心代码如下:
importUIKit
importWebKit
///WebViewPrintPageRenderer:usetoprintthefullcontentofwebviewintooneimage
internalfinalclassWebViewPrintPageRenderer:UIPrintPageRenderer{
privatevarformatter:UIPrintFormatter
privatevarcontentSize:CGSize
///生成PrintPageRenderer实例
///
///-Parameters:
///-formatter:WebView的viewPrintFormatter
///-contentSize:WebView的ContentSize
requiredinit(formatter:UIPrintFormatter,contentSize:CGSize){
self.formatter=formatter
self.contentSize=contentSize
super.init()
self.addPrintFormatter(formatter,startingAtPageAt:0)
}
overridevarpaperRect:CGRect{
returnCGRect.init(origin:.zero,size:contentSize)
}
overridevarprintableRect:CGRect{
returnCGRect.init(origin:.zero,size:contentSize)
}
privatefuncprintContentToPDFPage()->CGPDFPage?{
letdata=NSMutableData()
UIGraphicsBeginPDFContextToData(data,self.paperRect,nil)
self.prepare(forDrawingPages:NSMakeRange(0,1))
letbounds=UIGraphicsGetPDFContextBounds()
UIGraphicsBeginPDFPage()
self.drawPage(at:0,in:bounds)
UIGraphicsEndPDFContext()
letcfData=dataasCFData
guardletprovider=CGDataProvider.init(data:cfData)else{
returnnil
}
letpdfDocument=CGPDFDocument.init(provider)
letpdfPage=pdfDocument?.page(at:1)
returnpdfPage
}
privatefunccovertPDFPageToImage(_pdfPage:CGPDFPage)->UIImage?{
letpageRect=pdfPage.getBoxRect(.trimBox)
letcontentSize=CGSize.init(width:floor(pageRect.size.width),height:floor(pageRect.size.height))
//usuallyyouwantUIGraphicsBeginImageContextWithOptionslastparametertobe0.0asthiswillusthedevice'sscale
UIGraphicsBeginImageContextWithOptions(contentSize,true,2.0)
guardletcontext=UIGraphicsGetCurrentContext()else{
returnnil
}
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()
letimage=UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
returnimage
}
///printthefullcontentofwebviewintooneimage
///
///-Important:ifthesizeofcontentisverylarge,thenthesizeofimagewillbealsoverylarge
///-Returns:UIImage?
internalfuncprintContentToImage()->UIImage?{
guardletpdfPage=self.printContentToPDFPage()else{
returnnil
}
letimage=self.covertPDFPageToImage(pdfPage)
returnimage
}
}
extensionUIWebView{
publicfunctakeScreenshotOfFullContent(_completion:@escaping((UIImage?)->Void)){
self.scrollView.setContentOffset(CGPoint(x:0,y:0),animated:false)
DispatchQueue.main.asyncAfter(deadline:DispatchTime.now()+0.3){
letrenderer=WebViewPrintPageRenderer.init(formatter:self.viewPrintFormatter(),contentSize:self.scrollView.contentSize)
letimage=renderer.printContentToImage()
completion(image)
}
}
}
extensionWKWebView{
publicfunctakeScreenshotOfFullContent(_completion:@escaping((UIImage?)->Void)){
self.scrollView.setContentOffset(CGPoint(x:0,y:0),animated:false)
DispatchQueue.main.asyncAfter(deadline:DispatchTime.now()+0.3){
letrenderer=WebViewPrintPageRenderer.init(formatter:self.viewPrintFormatter(),contentSize:self.scrollView.contentSize)
letimage=renderer.printContentToImage()
completion(image)
}
}
}
WebViewPrintPageRenderer是该方案的核心类,负责把WebView组件内容打印到PDF,然后把PDF转换为图片。
UIWebView和WKWebView则实现对应的扩展。
测试代码:
//examplecode
privatefunctakeSnapshotOfUIWebView(){
self.uiWebView.scrollView.takeScreenshotOfFullContent{(image)in
//处理image
}
}
privatefunctakeSnapshotOfWKWebView(){
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截图示例查看具体截图效果。
以上三个方案,总的来说,解决了部分场景的需求,但都不够完美,仍需做进一步的优化。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。