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 截图示例查看具体截图效果。

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

总结

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

(0)

相关推荐

  • IOS实现手动截图并保存

    本文实例介绍了iOS手动剪裁图片并保存到相册的详细代码,分享给大家供大家参考,具体内容如下 一.实现效果 1.操作步骤 绘制一个矩形框,弹出一个alertView,提示是否保存图片 点击"是",将图片保存到相册 在相册中查看保存的图片 2.效果图 二.实现思路 1.在控制器的view上添加一个imageView,设置图片 2.在控制器的view上添加一个pan手势 3.跟踪pan手势,绘制一个矩形框(图片的剪切区域) 4.在pan手势结束时,通过alertView提示"是否将

  • 关于iOS截图你应该知道的那些事儿

    前言 同时按下 Home 键和电源键,咔嚓一声,就得到了一张手机的截图,这操作想必 iPhone 用户再熟悉不过了.我们作为研发人员,面对的是一个个的 View,那么该怎么用代码对 View 进行截图呢? 这篇文章主要讨论的是如何在包括 UIWebView 和 WKWebView 的网页中进行长截图,对应的示例代码在这儿:https://github.com/VernonVan/PPSnapshotKit (本地下载) . UIWebView 截图 对 UIWebView 截图比较简单,rend

  • iOS中如何获取某个视图的截图详析

    前言 最近在做SDK的截图,想触发类似系统的截屏功能,找了一圈,总结一下靠谱的几种方式. 我写了个UIView 的category,将这几种方式封装和简化了一下. 第一种情形截图 这种是最最普通的截图,针对一般的视图上添加视图的情况,基本都可以使用. 源码: /** 普通的截图 该API仅可以在未使用layer和OpenGL渲染的视图上使用 @return 截取的图片 */ - (UIImage *)nomalSnapshotImage { UIGraphicsBeginImageContext

  • iOS捕捉截屏事件并展示截图效果

    摩拜单车.微信的截屏就做的比较人性化. 现在很多APP开始支持用户截屏后,主动获取截图并弹出分享视图,这样用户就不用去相册去找了,感觉体验不错,今天就分享一下 截屏开发的心得,希望能帮助iOS的朋友. iOS7之后,苹果开放出一个通知:UIApplicationUserDidTakeScreenshotNotification,截屏时系统就会发出这个通知,需要你注册这个通知,就能捕捉到截屏图片. 下面的代码,实现的是用户截屏后,捕获到截屏图片,展示出来: //注册截屏通知 [[NSNotific

  • iOS 对view进行截图的示例代码

    本文主要介绍了iOS 对view进行截图的示例代码,分享给大家,具体如下: 需要对WKWebView进行截图,之前用的是下面的方法,高版本的系统是没有问题的,低版本的却截到一张白图 - (UIImage *)convertViewToImage:(UIView *)view{ // 第二个参数表示是否非透明.如果需要显示半透明效果,需传NO,否则YES.第三个参数就是屏幕密度了 UIGraphicsBeginImageContextWithOptions(CGSizeMake(view.boun

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

    前言 WebView就是一个内嵌浏览器控件,在iOS中主要有两种WebView:UIWebView和WKWebView,UIWebView是iOS2之后开始使用,WKWebView是在iOS8开始使用,WKWebView将逐步取代笨重的UIWebView. 由于项目需要,新近实现了一个长截图库 SnapshotKit.其中,需要支持 UIWebView.WKWebView 组件生成长截图.为了实现这个特性,查阅了很多资料,同时也做了不同的新奇思路尝试,最终实现了一个新的.取巧的技术方案. 以下主

  • java快速生成接口文档的三种解决方案

    目录 前言 方案一,使用japidocs 基本用法 方案2,swagger + knife4j 生成步骤 方案3,开源的接口文档生成工具 总结 前言 常常在项目收尾阶段,客户需要项目的接口文档,或者是一个大的sass平台,各个产品之间互相调用的时候,需要对方提供接口文档 通常来说,接口文档属于产品的技术沉淀,是一个长期积累的过程,然而,很多时候,开发阶段并不会想的那么多,结果到了需要接口文档的时候总是疲于应付,情急之下,往往采用最笨拙的办法,就是对照着项目代码,一个个拷贝吧 下面针对这个情况,小

  • iOS实现简单长截图

    本文实例为大家分享了iOS实现简易的长截图的具体代码,供大家参考,具体内容如下 长截图的实现原理: 实际上是将view的内容绘制成图片,再将各个view绘制出来的图片拼接出来. 具体代码: 将view绘制成图片 func getImage(in view:UIView?) -> UIImage? {     guard let view = view else {return nil}     let size = view.bounds.size     UIGraphicsBeginImag

  • Android WebView实现长按保存图片及长按识别二维码功能

    先来简单说一下本文所要实现的功能:用户在浏览网页的时候,长按某一区域,识别如果是图片,则弹出弹框,出现保存图片的功能.同时识别图片是否是二维码,如果是则在弹框中追加识别二维码功能. 细节上:保存图片的弹框要显示在手指长按的位置:选择图片保存后,可以让用户直接去相册查看:选择识别二维码,判断是是不是网址,是的话可以让用户选择复制或访问,否则可以让用户选择复制或搜索. 然后再来看一下效果图: 保存图片 save.gif 识别包含普通文字的二维码: text.gif 识别包含网址的二维码: code.

  • Android控件WebView实现完整截图

    最近总能看到好多APP都支持文章和网页的长截图,出于好奇研究了一下,分享给大家. 网上有好多的例子,其中好多都是已经过时的就不在复述了,我发现有一种还是比较通用的方法. //android 5.0 之后需要开启浏览器的整体缓存才能截取整个Web if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { WebView.enableSlowWholeDocumentDraw(); } 首先需要在WebView初始化之前(一般就是set

  • Android 后台生成长图并分享示例(非长截图)

    先看需求: 1.用户点击生成长图按钮,弹出等待框,后台生成一张长图. 2.用户展示界面和最终生成的长图,布局完全不一样,所以不能通过直接将view转换成bitmap,或者长截图来实现. 3.生成的长图,头部加上公司logo,尾部加上二维码. 难点分析: 1.后台进行. 2.长图保证清晰度,并且不能过大,过大可能会分享失败. 效果展示: 具体实现: 长图描述(纯手画,别介意 T_T) 1.准备数据: a.所需的文字内容 b.所需的图片(必须下载到本地,你可以开启一个线程进行图片的下载,在图片下载完

  • 详解IOS开发中生成推送的pem文件

    详解IOS开发中生成推送的pem文件 具体步骤如下: 首先,需要一个pem的证书,该证书需要与开发时签名用的一致. 具体生成pem证书方法如下: 1. 登录到 iPhone Developer Connection Portal(http://developer.apple.com/iphone/manage/overview/index.action )并点击 App IDs 2. 创建一个不使用通配符的 App ID .通配符 ID 不能用于推送通知服务.例如,  com.itotem.ip

  • iOS本地动态生成验证码的方法

    前几天app注册被人攻击了,从网上找了这个先保存下.... 用于ios本地动态生成验证码,效果如下: 导入CoreGraphics.framework 用于绘制图形 封装UIView,便捷使用,代码如下: AuthcodeView.h #import <UIKit/UIKit.h> @interface AuthcodeView : UIView @property (strong, nonatomic) NSArray *dataArray;//字符素材数组 @property (stron

  • iOS Webview自适应实际内容高度的4种方法详解

    //第一种方法 - (void)webViewDidFinishLoad:(UIWebView *)webView { CGFloat webViewHeight=[webView.scrollView contentSize].height; CGRect newFrame = webView.frame; newFrame.size.height = webViewHeight; webView.frame = newFrame; _webTablewView.contentSize = C

  • Android WebView自定义长按选择实现收藏/分享选中文本功能

    效果图(1.3M) 一.前言 ** 戳这里可以去DEMO,来吧 ** 相信刚接触android不久的同志们,在面对产品提出的 : "自定义WebView页面中,长按文本的弹出选项.点击选择后,分享.转发.收藏选择文本" 这样的需求时,第一反应大部分是:这是系统行为,如果实现需要在web端实现. 但是web端实现的局限性太大,曾经也有过监听系统粘贴板,在用户点击复制的时候实现其他的逻辑,但是这样用户体验不好,所以自定义WebView中长按的弹出菜单,并在点击时返回选中文本的小控件闪亮登场

随机推荐