WKWebView长按图片识别二维码

引子

在默认情况下,长按WKWebView中的图片,会弹出image save sheet

image save sheet

关于这个image save sheet,在官方文档中可以得到印证:

Safari Web Content Guide -> Handling Events -> One-Finger Events章节中有这样一句话:However, if the user touches and holds an image, the image save sheet appears instead of an information bubble.

此处使用的是WKWebView,但为什么要提到Safari相关的内容?原因很简单,iOS中的Safari应用、SFSafariViewControllerWKWebView,底层使用的是相同的东西。

Handling Events这一章中,有提及一些具体的事件、如何阻止事件的默认行为,以及支持的事件类型。

参考链接:Safari Web Content Guide 之 Handling Events

思路

为了实现长按图片识别二维码这样的功能,就必须将系统默认的image save sheet屏蔽掉。

思路如下:

  1. 在网页DOM加载完成后,注入JS脚本
  2. JS脚本中,找到所有的image元素,并为它们添加touch相关的事件
  3. 长按后阻止其默认行为,获取image元素的src,将结果传递给native进行处理:下载图片,并识别,判断其是否包含二维码
  4. 如果包含二维码,则弹出相关的提示

注意事项

上面说的是touch相关的事件,了解前端的开发者应该会知道mouse相关的事件,这两者在手机端上的表现,还是有区别的:当使用mouse相关的事件时,如果手指一直按在屏幕上时,会导致无法触发函数的执行,只有松开手指后才会触发,而touch相关的事件,则不会出现这样的情况。

实现

实现预期的效果,会用到jQuery、处理JS回调的WKScriptMessageHandler

约定ScriptMessageHandler名称

这里约定其名称为webImgLongPressHandler

1
2
3
4
5
6
7
8
private let scriptMessageHandlerName: String = "webImgLongPressHandler"

override func viewDidLoad() {
super.viewDidLoad()

let userContentController = WKUserContentController()
userContentController.add(self, name: scriptMessageHandlerName)
}

编写JS脚本

这里使用到了jQuery

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
$(document).ready(
function() {
var imageElements = document.images;
for(var i = 0; i < imageElements.length; i++) {
var imageElement = imageElements[i];
var intervalID = 0;
imageElement.ontouchstart = function(e) {
// 阻止默认行为
e.preventDefault();
// 长按时间设置为1秒
intervalID = window.setInterval(
function() {
window.clearInterval(intervalID);
// 将消息传递给native进行处理,这里使用的就是上面约定好的名称:webImgLongPressHandler
window.webkit.messageHandlers.webImgLongPressHandler.postMessage(e.target.src);
},
1000
);
};
imageElement.ontouchend = function(e) {
window.clearInterval(intervalID);
};
imageElement.ontouchcancel = function(e) {
window.clearInterval(intervalID);
}
};
}
);

将上面的脚本存放到名为image-element-long-press.js的文件中。

将脚本注入到WKWebView

1
2
3
4
5
6
7
8
9
10
11
12
13
let jqPath = Bundle.main.path(forResource: "jquery-3.1.1.min", ofType: "js")
let jsPath = Bundle.main.path(forResource: "image-element-long-press", ofType: "js")
let jqContent = try! String.init(contentsOfFile: jqPath!)
let jsContent = try! String.init(contentsOfFile: jsPath!)
let injectionContent = String.init(format: "%@\n%@", jqContent, jsContent)

let userScript = WKUserScript.init(source: injectionContent, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
userContentController.addUserScript(userScript)

let config = WKWebViewConfiguration.init()
config.userContentController = userContentController

let webView = WKWebView.init(frame: self.view.bounds, configuration: config)

处理识别

当长按图片时,就会发webImgLongPressHandler消息,native收到后进行处理:

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
// MARK: - WKScriptMessageHandler
public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == self.scriptMessageHandlerName {
if let urlString = message.body as? String {
if let url = URL.init(string: urlString) {
let sessionConfig = URLSessionConfiguration.default
let session = URLSession.init(configuration: sessionConfig)

let dataTask = session.dataTask(with: url, completionHandler: { (data, response, error) in
if data != nil {
let img = UIImage.init(data: data!)
let codes = self.QRcodesInImage(img)
if codes != nil && codes!.count > 0 {
print("QRcodes: \(self.QRcodesInImage(img))")

DispatchQueue.main.async {
// 弹出提示
let sheet = UIAlertController.init(title: nil, message: nil, preferredStyle: .actionSheet)
sheet.addAction(UIAlertAction.init(title: "识别二维码", style: .default, handler: { (action) in
var urls = [URL]()
for code in codes! {
if let url = URL.init(string: code) {
if UIApplication.shared.canOpenURL(url) {
urls.append(url)
}
}
}
let urlSheet = UIAlertController.init(title: nil, message: nil, preferredStyle: .actionSheet)
for url in urls {
let title = String.init(format: "打开%@", url.absoluteString)
urlSheet.addAction(UIAlertAction.init(title: title, style: .default, handler: { (action) in
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}))
}
urlSheet.addAction(UIAlertAction.init(title: "取消", style: .cancel, handler: { (action) in

}))
self.present(urlSheet, animated: true, completion: nil)
}))
sheet.addAction(UIAlertAction.init(title: "取消", style: .cancel, handler: { (action) in

}))
self.present(sheet, animated: true, completion: nil)
}
}
}
})
dataTask.resume()
}
}
}
}

此处使用ZBarSDK进行二维码的识别(使用系统提供的API时,对于一张图片上有多个二维码的情况,虽然CIDetectorfeaturesInImage方法返回的是数组,但在测试时,调整了参数,返回的数组一直只包含一个元素):

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
private func QRcodesInImage(_ image: UIImage?) -> Array<String>? {
if image == nil {
return nil
}
let scanner = ZBarImageScanner()
let barImg = ZBarImage.init(cgImage: image!.cgImage)
let count = scanner.scanImage(barImg)
if count == 0 {
return nil
}
var codes = [String]()
let symbolSet = scanner.results
var symbol: OpaquePointer? = nil
for i in 0 ..< symbolSet!.count {
if i == 0 {
symbol = zbar_symbol_set_first_symbol(symbolSet!.zbarSymbolSet)
} else if symbol != nil {
symbol = zbar_symbol_next(symbol)
}
if symbol != nil {
let data = zbar_symbol_get_data(symbol)
if let code = String.init(utf8String: data!) {
codes.append(code)
}
}
}
return codes.count > 0 ? codes : nil
}

-webkit-touch-callout

在阻止默认行为时,上面的示例中使用的是preventDefault()函数。也有另外一个选择,就是使用-webkit-touch-callout

参见苹果官方文档:

Safari CSS Reference -> Supported CSS Properties -> User Interface

链接:Safari CSS Reference: -webkit-touch-callout

其它参考:

语法可参考:

也就是:

1
document.getElementById("id").style.property="值"

backgroundColorbackground-color是对应的,类似地,应是webkitTouchCallout-webkit-touch-callout相对应。

使用时,将其设置为none

1
imageElement.style.webkitTouchCallout = "none";

需要注意的是,preventDefault()-webkit-touch-callout,在效果上是有区别的。

二者都可以阻止默认的save image sheet

不同点如下:

  • 图片上产生的事件使用preventDefault()后,就无法选择该图片元素,无法通过双击该图片缩放网页

  • 将图片的样式中的-webkit-touch-callout设置为none,可以选择该图片元素,可以通过双击该图片缩放网页

个人觉得,在这种需求下,长按后界面中出现复制Menu是不太好看的,因此建议使用preventDefault()

附上代码链接:
https://github.com/Daniate/WKWebViewQRcode


WKWebView长按图片识别二维码
https://daniate.github.io/2017/03/21/WKWebView长按图片识别二维码/
作者
Daniate
发布于
2017年3月21日
许可协议