服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - IOS - iOS使用WebView生成长截图的第3种解决方案

iOS使用WebView生成长截图的第3种解决方案

2021-05-13 16:55York_魚 IOS

这篇文章主要给大家介绍了关于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由于还没把网页的内容加载出来,导致生成的长截图是空白的。

方案一核心代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
 }
}

测试代码:

?
1
2
3
4
5
// example code
 private func takeSnapshotOfUIWebView() {
 let image = self.webView.scrollView.takeSnapshotOfFullContent()
 // 处理image
}

方案二:分页截图组件内容,合成长图

方案二的实现要点在于:分页滚动WebView组件的内容,然后生成分页截图,最后把所有分页截图合成一张长图。

这个方案适用于 UIWebView 组件和 WKWebView 组件。

方案二核心代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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()
   }
  }
 }
}

测试代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
// 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转换成图片

新方案的核心代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
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 则实现对应的扩展。

测试代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
// 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 截图示例查看具体截图效果。

以上三个方案,总的来说,解决了部分场景的需求,但都不够完美,仍需做进一步的优化。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对服务器之家的支持。

原文链接:https://juejin.im/post/5b9d145ae51d450e7579d1e5

延伸 · 阅读

精彩推荐
  • IOSIOS开发之字典转字符串的实例详解

    IOS开发之字典转字符串的实例详解

    这篇文章主要介绍了IOS开发之字典转字符串的实例详解的相关资料,希望通过本文能帮助到大家,让大家掌握这样的方法,需要的朋友可以参考下...

    苦练内功5832021-04-01
  • IOS解析iOS开发中的FirstResponder第一响应对象

    解析iOS开发中的FirstResponder第一响应对象

    这篇文章主要介绍了解析iOS开发中的FirstResponder第一响应对象,包括View的FirstResponder的释放问题,需要的朋友可以参考下...

    一片枫叶4662020-12-25
  • IOSIOS 屏幕适配方案实现缩放window的示例代码

    IOS 屏幕适配方案实现缩放window的示例代码

    这篇文章主要介绍了IOS 屏幕适配方案实现缩放window的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要...

    xiari5772021-06-01
  • IOSiOS通过逆向理解Block的内存模型

    iOS通过逆向理解Block的内存模型

    自从对 iOS 的逆向初窥门径后,我也经常通过它来分析一些比较大的应用,参考一下这些应用中某些功能的实现。这个探索的过程乐趣多多,不仅能满足自...

    Swiftyper12832021-03-03
  • IOSiOS中tableview 两级cell的展开与收回的示例代码

    iOS中tableview 两级cell的展开与收回的示例代码

    本篇文章主要介绍了iOS中tableview 两级cell的展开与收回的示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...

    J_Kang3862021-04-22
  • IOSiOS 雷达效果实例详解

    iOS 雷达效果实例详解

    这篇文章主要介绍了iOS 雷达效果实例详解的相关资料,需要的朋友可以参考下...

    SimpleWorld11022021-01-28
  • IOSiOS布局渲染之UIView方法的调用时机详解

    iOS布局渲染之UIView方法的调用时机详解

    在你刚开始开发 iOS 应用时,最难避免或者是调试的就是和布局相关的问题,下面这篇文章主要给大家介绍了关于iOS布局渲染之UIView方法调用时机的相关资料...

    windtersharp7642021-05-04
  • IOS关于iOS自适应cell行高的那些事儿

    关于iOS自适应cell行高的那些事儿

    这篇文章主要给大家介绍了关于iOS自适应cell行高的那些事儿,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的...

    daisy6092021-05-17