Swift与H5交互:使用WebViewJavascriptBridge

3,177 阅读5分钟

这是我参与更文挑战的第10天,活动详情查看: 更文挑战

前言

之前上天,我分别讲解了iOS原生、Flutter和H5的交互,今天来介绍的是通过第三方框架来进行iOS与H5的互调。

这个第三方框架来头不小,资历老,在UIWebView时代,我曾经就用过它进行OC与JS的互调工作。它就是WebViewJavascriptBridge

来,我上图让大家感受一下:

image.png

gitignore创建于8年前。

image.png

最后一次更新与2017年11月,离现在也有4、5年的时间了。

为何这个框架如此之久没有更新了仍然可用,我个人认为有以下两点原因:

  • 1.WKWebView推出之后,苹果这边再也没有针对Web这块推出新的模块与组件,虽然随着iOS的版本迭代,WKWebView会增加一些新的Api,但是核心的iOS与H5的交互核心与业务没有改变。

  • 2.WebViewJavascriptBridge是由Objective-C语言编写,这么语言最大的特色就是基本不会有什么语言更新,非常稳定。

基于以上两个原因,这就是WebViewJavascriptBridge依旧可用的原因。

WebViewJavascriptBridge的使用

本篇会附上通过修改WebViewJavascriptBridge官方提供的example.html,使得JS调起原生摄像头,原生获取图像数据后,主动调用JS方法,将图片数据传递给JS端的代码与例子。

先上JS这边的代码,重要的地方我会写注释,请仔细看,另外我询问过我的前端小伙伴,说这代码比较上古,大家主要看<scrpit>里面的逻辑实现。

Html侧代码:

<!doctype html>
<html>
    <head>
        <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0">
        <style type='text/css'>
            html { font-family:Helvetica; color:#222; }
            h1 { color:steelblue; font-size:24px; margin-top:24px; }
            button { margin:0 3px 10px; font-size:12px; }
            .logLine { border-bottom:1px solid #ccc; padding:4px 2px; font-family:courier; font-size:11px; }
        </style>
    </head>
    <body>
        <h1>WebViewJavascriptBridge Demo</h1>
        <div id="images"></div>
        <script>
        window.onerror = function(err) {
            log('window.onerror: ' + err)
        }

        function setupWebViewJavascriptBridge(callback) {
            if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
            if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
            window.WVJBCallbacks = [callback];
            var WVJBIframe = document.createElement('iframe');
            WVJBIframe.style.display = 'none';
            WVJBIframe.src = 'https://__bridge_loaded__';
            document.documentElement.appendChild(WVJBIframe);
            setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
        }

        setupWebViewJavascriptBridge(function(bridge) {
            var uniqueId = 1
            function log(message, data) {
                var log = document.getElementById('log')
                var el = document.createElement('div')
                el.className = 'logLine'
                el.innerHTML = uniqueId++ + '. ' + message + ':<br/>' + JSON.stringify(data)
                if (log.children.length) { log.insertBefore(el, log.children[0]) }
                else { log.appendChild(el) }
            }
            /// 这里是JS这边注册一个nativeImageData句柄
            /// 用于原生调用该方法,主动将原生获取相机数据传递过来
            bridge.registerHandler('nativeImageData', function(data, responseCallback) {
                log('Swift 调用 JS nativeImageData with', data)
                
                let imageString = data.imgData
                
                var d1=document.getElementById("images")
                var img=document.createElement("img")
                img.src= "data:image/png;base64,"+imageString
                d1.appendChild(img)
                
                var responseData = { 'JS':'Get the Image' }
                log('JS responding with', responseData)
                responseCallback(responseData)
            })

            document.body.appendChild(document.createElement('br'))

            var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
            callbackButton.innerHTML = '调用原生相机'
            callbackButton.onclick = function(e) {
                e.preventDefault()
                log('JS 调用 Swift')
                /// 这里是JS调用一个叫jsCallNativeTakePhoto的句柄
                /// 其目的是让在原生注册过jsCallNativeTakePhoto的位置,进行JS调用原生。
                bridge.callHandler('jsCallNativeTakePhoto', {'message': 'take a photo'}, function(response) {
                    log('JS got response', response)
                })
            }
        })
        </script>
        <div id='buttons'></div> <div id='log'></div>
    </body>
</html>

iOS侧代码

import UIKit
import WebKit
import AVFoundation

import WebViewJavascriptBridge

/// 定义一个bridge,用于原生与H5侧互调
private var bridge: WebViewJavascriptBridge!

class BridgeWebViewController: UIViewController {
    
    /// 懒加载webView
    private lazy var webView: WKWebView = {
        let config = WKWebViewConfiguration()
        let preferences = WKPreferences()
        preferences.javaScriptCanOpenWindowsAutomatically = true
        config.preferences = preferences
        
        let webView = WKWebView(frame: view.frame, configuration: config)
        webView.allowsBackForwardNavigationGestures = true
        webView.navigationDelegate = self
        return webView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        bridgeSetting()
        setupUI()
    }  
}

extension BridgeWebViewController {
    func setupUI() {
        view.backgroundColor = .white
        view.addSubview(webView)
        
        /// 调用本地的html
        let htmlPath = Bundle.main.path(forResource: "ExampleApp", ofType: "html")
        let appHtml = try! String(contentsOfFile: htmlPath!, encoding: .utf8)
        let baseURL = URL(fileURLWithPath: htmlPath!)
        webView.loadHTMLString(appHtml, baseURL: baseURL)
    }
    
    /// bridge的配置
    func bridgeSetting() {
        if let _ = bridge {
            return
        }
        
        WebViewJavascriptBridge.enableLogging()
        
        /// 初始化bridge
        bridge = WebViewJavascriptBridge(forWebView: webView)
        
        /// 设置代理
        bridge.setWebViewDelegate(self)
        
        /// 通过bridge注册监听JS的句柄jsCallNativeTakePhoto
        bridge.registerHandler("jsCallNativeTakePhoto") { [weak self] any, responseCallback in
        
            /// 这里面写的就是监听到JS的方法后,在原生侧的方法实现
            /// 这里我干了两件事:1.回调给JS侧一个字符串, 2.打开相机
            
            print("jsCallNativeTakePhoto:\(any)")
            responseCallback?("这里是从Swift回调给JS的字符串")
            
            guard let self = self else { return }
            /// 打开相机弹窗,这里是通过一个分类单独写的,后面放上。
            self.showSystemImageAlertControll(delegate: self)
        }
    }
    
    /// 通过bridge调用JS端的函数,将原生数据传递给
    private func takeImageToJS(imageBase64String: String, type : Int) {
        bridge.callHandler("nativeImageData", data: ["imgData": imageBase64String, "type": type])
    }
}

/// UIImagePickerControllerDelegate回调图片数据
extension BridgeWebViewController: UIImagePickerControllerDelegate {
    
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        picker.dismiss(animated: true)
    }
    
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        /// 图片转二进制
        guard let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage,
              let data = image.pngData() else {
            return
        }
        /// 二进制转为base64字符串
        let string = data.base64EncodedString()
        
        /// 在picker消失的时候,将原生侧的数据传递给JS侧
        picker.dismiss(animated: true) {
            self.takeImageToJS(imageBase64String: string, type: 0)
        }
    }
}

extension BridgeWebViewController: UINavigationControllerDelegate {}

/// 监听WebView加载的生命周期,按需使用,这里只是写了这两个。
extension BridgeWebViewController: WKNavigationDelegate {
    
    /// 页面开始加载时调用
    ///
    /// - Parameters:
    ///   - webView: 实现该代理的webview
    ///   - navigation: 当前navigation
    func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
        
    }
    

    /// 页面加载完成之后调用
    ///
    /// - Parameters:
    ///   - webView: 实现该代理的webview
    ///   - navigation: 当前navigation
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        
    }
}

432ef3913f1d614e703ca0a49686e62b.jpeg

详细的注释我都写在代码块里面了,使用这个框架,其实针对原生与JS都做了约束,原生和JS都有相同的两个方法registerHandlercallHandler,分别表示注册监听调用,相当于在个各端保证Api的一致性,并行通过回调,可以将有没有收到另一侧的消息通过回调的形式返回过去。


WebViewJavascriptBridge没有什么在用法上没有什么太多需要说明的地方,原生侧与JS侧约定好方法名,入参形式,就可以进行调试了。

Android侧可以通过JsBridge与H5通信,用于iOS、Android、H5在方法层面上的大统一。

WebViewJavascriptBridge使用的注意事项:

上面已经说过,WebViewJavascriptBridge最后一次更新是2017年,而其中的代码还兼容了UIWebView,而目前UIWebView在iOS已经过期,如果使用,在上架时会被拒。

建议将WebViewJavascriptBridge中涉及UIWebView的部分删除后再进行集成。

这个框架也算是在Swift中调用OC代码的典型代表。

最后附上调用系统相机与相册的UIViewController分类,CV即用

import UIKit
import AVFoundation

/// 系统相册调用的协议
public typealias SystemImagePickerInterface = UIImagePickerControllerDelegate & UINavigationControllerDelegate

// MARK: - 系统相册的调用
extension UIViewController {
    
    //MARK:- 显示alertController,相册选择,拍照,取消的方法
    
    /// 显示alertController
    ///
    /// - Parameter delegate: 需要设置代理的类
    public func showSystemImageAlertControll(delegate: SystemImagePickerInterface) {
        let imagePicker = UIImagePickerController()
        imagePicker.navigationBar.barTintColor = navigationController?.navigationBar.barTintColor
        imagePicker.navigationBar.tintColor = navigationController?.navigationBar.tintColor
        imagePicker.navigationBar.titleTextAttributes = navigationController?.navigationBar.titleTextAttributes
        
        imagePicker.delegate = delegate
        imagePicker.allowsEditing = true
        
        let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
        
        // 拍照
        let takePhotonAction = UIAlertAction(title: "拍照", style: .default) {
            (action: UIAlertAction) in
            self.takePhoto(imagePicker: imagePicker)
        }
        
        // 从相册中选择
        let selectFromPhotoAlbumAction = UIAlertAction(title: "从相册中选择", style: .default) {
            (action: UIAlertAction) in
            self.selectFromPhotoAlbum(imagePiker: imagePicker)
        }
        
        // 取消
        let cancelAction: UIAlertAction = UIAlertAction(title: "取消", style: .cancel, handler: nil)
        
        alertController.addAction(takePhotonAction)
        alertController.addAction(selectFromPhotoAlbumAction)
        alertController.addAction(cancelAction)
        present(alertController, animated: true, completion: nil)
    }
    
    //MARK:- 选取图片的具体方法
    
    /// 拍照获取图片的方法
    ///
    /// - Parameter imagePicker: UIImagePickerController
    private func takePhoto(imagePicker: UIImagePickerController) {
        imagePicker.sourceType = .camera
        
        useFrontOrRearCamera(imagePiker: imagePicker)
        
        let status: AVAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: AVMediaType.video)
        
        switch status {
        case .authorized:
            present(imagePicker, animated: true, completion: nil)
        case .denied:
            break
        case .restricted:
            break
        case .notDetermined:
            present(imagePicker, animated: true, completion: nil)
        @unknown default:
            break
        }
    }
    
    /// 从相册获取图片的方法
    ///
    /// - Parameter imagePiker: UIImagePickerController
    private func selectFromPhotoAlbum(imagePiker: UIImagePickerController) {
        imagePiker.sourceType = .photoLibrary
        present(imagePiker, animated: true, completion: nil)
    }
    
    /// 使用前置或者后置摄像头的方法,通过调整判断前置与后置的顺序可以改变调用的优先级
    ///
    /// - Parameter imagePiker: UIImagePickerController
    private func useFrontOrRearCamera(imagePiker: UIImagePickerController) {
        if UIImagePickerController.isCameraDeviceAvailable(.rear) {
            imagePiker.cameraDevice = .rear
        }else if UIImagePickerController.isCameraDeviceAvailable(.front) {
            imagePiker.cameraDevice = .front
        }else {
            return
        }
    }
}

明日继续

转眼明天就是周五,马上就要端午节了,本周因为工作原因,主要介绍了Swift与H5交互,下一步主要介绍网络请求模块,也就是Alamofire、Moya、Codable。

在介绍这一块之前,可能会先讲解一下枚举和其他小功能。

大家加油!