阅读 1642
Swift与H5交互:Swift与JS方法互调

Swift与H5交互:Swift与JS方法互调

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

H5的需求

  • 1.跳转到加载到H5的控制器后,系统的NavigationBar需要隐藏,而导航栏由WebView加载的H5渲染,点击H5上的返回按钮,进行页面的pop操作。

  • 2.在加载完的WebView页面,点击H5上的按钮,调起原生的相机应用(至于是系统相机,还是定制化UI的相机,由原生自己控制),使用相机拍照完成后,将图片通过base64字符串的形式传递给H5。

  • 3.加载完成WebView后,需要将之前原生获取的token传递到H5端。

原生分析与方案确定:

  • 第一个例子,点击H5的按钮,然后使得原生界面关闭是典型的JS调用原生。

  • 第二个例子,就是典型的互调,先有JS调起原生相机,而后原生在获取到图片资源后,调用JS方法,将原生里的数据传递给JS。

  • 第三个例子和第一个例子是一样的,也是JS调用原生,不过需要注意的WebView的生命周期,注意是WebView的生命周期,我在调试的时候饶了点弯路,稍后分享。

JS调用原生方法

既然已经分析完了到底是谁调用谁,那么接下来就是正确的调用函数即可了。

WKWebViewConfiguration类中有个属性userContentController

open class WKWebViewConfiguration : NSObject, NSSecureCoding, NSCopying {

    /** @abstract The user content controller to associate with the web view.
    */
    open var userContentController: WKUserContentController
}
复制代码

userContentController属性是专门用来监听JS调用方法的,而userContentController的类WKUserContentController中,有一个专门监听JS方法句柄的方法

open class WKUserContentController : NSObject, NSSecureCoding {

    
    /** @abstract Adds a script message handler to the main world used by page content itself.
     @param scriptMessageHandler The script message handler to add.
     @param name The name of the message handler.
     @discussion Calling this method is equivalent to calling addScriptMessageHandler:contentWorld:name:
     with [WKContentWorld pageWorld] as the contentWorld argument.
     */
    open func add(_ scriptMessageHandler: WKScriptMessageHandler, name: String)
}
复制代码

WKScriptMessageHandler是需要设置的代理,一般是WebView所在的控制器,name就是JS的方法句柄。

需要注意的是这个WKScriptMessageHandler代理,最好是进行封装一层,避免循环引用,代码如下

import Foundation
import WebKit

class WeakScriptMessageDelegate: NSObject {

    //MARK:- 属性设置
    private weak var scriptDelegate: WKScriptMessageHandler!
    
    //MARK:- 初始化
    init(scriptDelegate: WKScriptMessageHandler) {
        self.scriptDelegate = scriptDelegate
    }
}

extension WeakScriptMessageDelegate: WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        self.scriptDelegate.userContentController(userContentController, didReceive: message)
    }
}
复制代码

接下来,我们就可以将这个配置项赋值给WebView了

private lazy var webView: WKWebView = {
        /// 生成配置项
        let config = WKWebViewConfiguration()
        config.userContentController.add(WeakScriptMessageDelegate(scriptDelegate: self), name: "SeasonCallback")
        let preferences = WKPreferences()
        preferences.javaScriptCanOpenWindowsAutomatically = true
        config.preferences = preferences
        
        /// webView初始化时,入参配置项
        let webView = WKWebView(frame: view.frame, configuration: config)
        webView.allowsBackForwardNavigationGestures = true
        /// webView的监听
        webView.navigationDelegate = self
        return webView
 }()
复制代码

然后,我们在控制器这一层实现WKScriptMessageHandler的代理:

extension WebViewController: WKScriptMessageHandler {
    
    /// 原生界面监听JS运行,截取JS中的对应在userContentController注册过的方法
    ///
    /// - Parameters:
    ///   - userContentController: WKUserContentController
    ///   - message: WKScriptMessage 其中包含方法名称已经传递的参数,WKScriptMessage,其中body可以接收的类型是Allowed types are NSNumber, NSString, NSDate, NSArray, NSDictionary, and NSNull
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        print("方法名:\(message.name)")
        print("参数:\(message.body)")
        
        guard let msg = message.body as? [String: String], let value = msg["method"] else { return }
        
        if value == "back" {
            navigationController?.popViewController(animated: true)
        }
    }
}
复制代码

在HTML,我们需要注册这样一个JS方法:

JS中messageHandlers后注册的方法名必须和在原生注册的方法名一模一样!这里是SeasonCallback。

对应上面的原生方法的注释,WKScriptMessage中的body,实际就是js传递过来的参数argument,我个人比较偏向于在JS中传递JSON,这样在原生中转Dictionary一来方便,二来可以传递多个参数

下面的方法和H5中按钮组件点击事件进行绑定:

function jsButtonAction() {
    let argument = {
                        'method' : 'back',
                        'params' : {}
                    };

    window.webkit.messageHandlers.SeasonCallback.postMessage(argument);
}
复制代码

这样JS调用原生函数的通道就打通了,我们可以注册多个句柄,或者在传参中method传入不同的值来区分不同的方法。

要注意的是,为了避免循环引用,需要的控制器的析构函数中,将注册监听的JS句柄移除:

deinit {
    webView.configuration.userContentController.removeScriptMessageHandler(forName: "SeasonCallback")
}
复制代码

原生调用JS方法

调用方式

原生调JS简单一点,只用调用WebView这个方法就可以了:

extension WKWebView {

    public func evaluateJavaScript(_ javaScript: String, in frame: WKFrameInfo? = nil, in contentWorld: WKContentWorld, completionHandler: ((Result<Any, Error>) -> Void)? = nil)

}
复制代码

比如我们在H5中的JS编写这样一个函数:

function callJS(text) {
    console.log(text);
    return "你好,Swift!";
}
复制代码

在Swift中这样调用

webView.evaluateJavaScript("callJS('这是从Swift传递到JS的参数')") { any, error in
    print(any)
    print(error)
}

复制代码

调用时机

之前讲第三个需求加载完成WebView后,需要将之前原生获取的token传递到H5端。

我们项目由于前端是用Vue写的,在main.js里添加了这样一段代码,我理解的是用钩子注册了一个方法:

window.getToken = function(token) {
  Vue.prototype.$token = token;
}
复制代码

而我在WebView所在的控制中添加了这样一段代码:

override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = .white

    view.addSubview(webView)

    if let url = URL(string: url) {
        let request = URLRequest(url: url)
        webView.load(request)

        webView.evaluateJavaScript("getToken('\(token)')") { any, error in
            print(any)
            print(error)
        }
    }
}
复制代码

我在webView.load(request)后就立即执行了运行JS方法的函数,而前端的朋友根本就拿不到token!!!

为什么?!因为我理解的是在原生的viewDidLoad一旦添加了webView并加载了request,就可以运行 webView.evaluateJavaScript了。但实际上,Vue的生命周期中window.getToken = function(token) { Vue.prototype.$token = token; }这段代码是页面初始化后,该方法才被注册进去的。

所以我要调用这个JS方法,必须等到WebView加载完成之后才行!!!

给WebView添加WKNavigationDelegate代理,

webView.navigationDelegate = self
复制代码

在WKNavigationDelegate代理中,func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)该回调中执行evaluateJavaScript就可以正确的将token传递给H5页面。


extension WebViewController: WKNavigationDelegate { 
    /// 页面加载完成之后调用
    ///
    /// - Parameters:
    ///   - webView: 实现该代理的webview
    ///   - navigation: 当前navigation
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        webView.evaluateJavaScript("CUSCGetToken('\(token)')") { any, error in
            print(any)
            print(error)
        }
    }
}
复制代码

总结

  • Swift与JS方法互调并没有特别难的地方,首先需要明确就是谁调用谁,一旦明确这个是Swift调用JS,抑或是JS调用Swift,那么剩下来的就是使用正确的方法了。

  • 在Swift与JS制定好方法名,传参格式,严格按照双方开发人员制定的规则传递函数与参数,这样才能成功调起函数并通信。

  • 了解一些Html的初始化周期,有利于把握Swift调用JS的时机。

明日继续

明天可能会插播一下Flutter与JS交互的方案与例子,大家加油。

文章分类
iOS
文章标签