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截图示例查看具体截图效果。
以上三个方案,总的来说,解决了部分场景的需求,但都不够完美,仍需做进一步的优化。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。