一间杂货铺~

03月 21

WKWebView长按图片识别二维码


I. 引子

在默认情况下,长按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

II. 思路

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

思路如下:

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

注意事项

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

III. 实现

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

约定ScriptMessageHandler名称

这里约定其名称为webImgLongPressHandler

private let scriptMessageHandlerName: String = "webImgLongPressHandler"

    override func viewDidLoad() {
        super.viewDidLoad()

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

编写JS脚本

这里使用到了jQuery

$(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

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收到后进行处理:

// 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方法返回的是数组,但在测试时,调整了参数,返回的数组一直只包含一个元素):

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

其它参考:

语法可参考:

也就是:

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

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

使用时,将其设置为none

imageElement.style.webkitTouchCallout = "none";

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

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

不同点如下:

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

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

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

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

标签:none

还不快抢沙发

添加新评论