当前位置: 移动技术网 > IT编程>移动开发>IOS > iOS使用WebView生成长截图的第3种解决方案

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

2019年07月23日  | 移动技术网IT编程  | 我要评论

钢水温度,康熙奉茶宫女,pinkypinky

前言

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

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

总结

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

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

  • ios uicollectionview实现横向滚动

    现在使用卡片效果的app很多,之前公司让实现一种卡片效果,就写了一篇关于实现卡片的文章。文章最后附有demo实现上我选择了使用uicollectionview ... [阅读全文]
  • iOS UICollectionView实现横向滑动

    本文实例为大家分享了ios uicollectionview实现横向滑动的具体代码,供大家参考,具体内容如下uicollectionview的横向滚动,目前我使... [阅读全文]
  • iOS13适配深色模式(Dark Mode)的实现

    iOS13适配深色模式(Dark Mode)的实现

    好像大概也许是一年前, mac os系统发布了深色模式外观, 看着挺刺激, 时至今日用着也还挺爽的终于, 随着iphone11等新手机的发售, ios 13系统... [阅读全文]
  • ios 使用xcode11 新建项目工程的步骤详解

    ios 使用xcode11 新建项目工程的步骤详解

    xcode11新建项目工程,新增了scenedelegate这个类,转而将原appdelegate负责的对ui生命周期的处理担子接了过来。故此可以理解为:ios... [阅读全文]
  • iOS实现转盘效果

    本文实例为大家分享了ios实现转盘效果的具体代码,供大家参考,具体内容如下demo下载地址: ios转盘效果功能:实现了常用的ios转盘效果,轮盘抽奖效果的实现... [阅读全文]
  • iOS开发实现转盘功能

    本文实例为大家分享了ios实现转盘功能的具体代码,供大家参考,具体内容如下今天给同学们讲解一下一个转盘选号的功能,直接上代码直接看viewcontroller#... [阅读全文]
  • iOS实现轮盘动态效果

    本文实例为大家分享了ios实现轮盘动态效果的具体代码,供大家参考,具体内容如下一个常用的绘图,主要用来打分之类的动画,效果如下。主要是ios的绘图和动画,本来想... [阅读全文]
  • iOS实现九宫格连线手势解锁

    本文实例为大家分享了ios实现九宫格连线手势解锁的具体代码,供大家参考,具体内容如下demo下载地址:效果图:核心代码://// clockview.m// 手... [阅读全文]
  • iOS实现卡片堆叠效果

    本文实例为大家分享了ios实现卡片堆叠效果的具体代码,供大家参考,具体内容如下如图,这就是最终效果。去年安卓5.0发布的时候,当我看到安卓全新的material... [阅读全文]
  • iOS利用余弦函数实现卡片浏览工具

    iOS利用余弦函数实现卡片浏览工具

    本文实例为大家分享了ios利用余弦函数实现卡片浏览工具的具体代码,供大家参考,具体内容如下一、实现效果通过拖拽屏幕实现卡片移动,左右两侧的卡片随着拖动变小,中间... [阅读全文]
验证码:
移动技术网