JS&Swift相互交互

  1. 加载本地HTML文件
 
 
 
x
 
 
 
 
override func loadView() {
    super.loadView()
    let conf = WKWebViewConfiguration()
  //JS调用HTML时使用的name
    conf.userContentController.add(self, name: "wkbridge")
    self.wk = WKWebView(frame: CGRect(x: 0, y:20, width: self.view.frame.size.width, height: self.view.frame.size.height - 20), configuration: conf)
    self.wk.navigationDelegate = self
    self.wk.uiDelegate = self
    if let path = Bundle.main.path(forResource: "index", ofType: "html", inDirectory: "www") {
        let fileUrl = URL(fileURLWithPath: path)
        self.wk.loadFileURL(fileUrl, allowingReadAccessTo: fileUrl)
    }
    
    //每个页面注入 JS 插件代码 (runPluginJS方法在下面)
    self.runPluginJS(names: ["Base", "Console", "Sandbox"])
    //注入页面间传递的参数
    if pageParam != nil {
        self.wk.evaluateJavaScript("win.pageParam=\(pageParam!)", completionHandler: nil)
    }
    self.view.addSubview(self.wk)
    
    //添加一个等待指示器
    self.view.backgroundColor = UIColor.white
    activityIndicator = UIActivityIndicatorView()
    activityIndicator.center = CGPoint(x: self.view.bounds.width/2, y: self.view.bounds.height/2)
    activityIndicator.color = UIColor.black
    activityIndicator.startAnimating()
    self.view.addSubview(activityIndicator)
}
//注入插件文件
func runPluginJS(names: Array<String>) {
    for name in names {
        if let path = Bundle.main.path(forResource: name, ofType: "js") {
            do {
                let js = try NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue)
                self.wk.evaluateJavaScript(js as String, completionHandler: nil)
            } catch let error as NSError {
                print(error.debugDescription)
            }
        }
    }
}
 

在项目根目录中新建 www 文件夹(自定义) 放入 html js css 图片 文件,前端文件都放在www中,方便管理

注意

将wkwebview需要访问的本地文件 添加到项目的 Bundle Resources

  1. HTML 与 Native 交互

ViewController 实现 三个协议 WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler

  • JS调用Swift代码

    js直接使用

 
 
 
xxxxxxxxxx
 
 
 
 
// wkbridge为在WKWebViewConfiguration定定义的name
// js传递的param使用json对象 ,比如类似{className: "Console", functionName: "log", data: {msg: "来自js的console"}}
window.webkit.messageHandlers.wkbridge.postMessage(param);
 

会触发swift方法

 
 
 
xxxxxxxxxx
 
 
 
 
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    if message.name == "wkbridge" {
        if let dic = message.body as? NSDictionary,
           let className = (dic["className"] as AnyObject).description,
           let functionName = (dic["functionName"] as AnyObject).description {
            if let cls = NSClassFromString((Bundle.main.object(forInfoDictionaryKey: "CFBundleName")! as AnyObject).description + "." + className) as? Plugin.Type{
                let obj = cls.init()
                obj.viewController = self
                obj.wk = self.wk
                obj.taskId = (dic["taskId"] as AnyObject).integerValue
                obj.data = (dic["data"] as AnyObject) as? NSDictionary
                let functionSelector = Selector(functionName)
                if obj.responds(to: functionSelector) {
                    obj.perform(functionSelector)
                } else {
                    print("Undefined function :\(functionName)")
                }
            } else {
                print("Class Not Found: \(className)")
            }
        }
    }
}
 
  • js的方法在 Xcode的控制台打印内容

在swift新建类Console.swift

 
 
 
xxxxxxxxxx
 
 
 
 
import UIKit
class Console: Plugin {
    func log() {
        if let string = self.data?["msg"] {
            print(string)
        }
    }
}
 

这里使用到一个基类 Plugin 用来处理 js 与 swift的交互

 
 
 
xxxxxxxxxx
 
 
 
 
import UIKit
import WebKit
class Plugin: NSObject {
    var viewController: MeiWebView!
    var wk: WKWebView!
    var taskId: Int!
    var data: NSDictionary?
    required override init() {
    }
    func callback(values: NSDictionary) -> Bool {
        do {
            let jsonData = try JSONSerialization.data(withJSONObject: values, options: JSONSerialization.WritingOptions())
            if let jsonString = NSString(data: jsonData, encoding: String.Encoding.utf8.rawValue) as? String,
               let tTaskId = self.taskId{
                let js = "fireTask(\(tTaskId), '\(jsonString)');"
                self.wk.evaluateJavaScript(js, completionHandler: nil)
                return true
            }
        } catch let error as NSError{
            NSLog(error.debugDescription)
            return false
        }
        return false
    }
    func errorCallback(errorMessage: String) {
        let js = "onError(\(self.taskId), '\(errorMessage)');"
        self.wk.evaluateJavaScript(js, completionHandler: nil)
    }
    
    func convertToDictionary(text: String) -> [String: Any]? {
        if let data = text.data(using: .utf8) {
            do {
                print(text)
                print(data)
                return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
            } catch {
                print("JSON 转换失败 :" + error.localizedDescription)
            }
        }
        return nil
    }   
}
 

这样js的方法就会在 Xcode的控制台打印出 内容了

 

对于隐藏接口,给前段一个友好的方式,使用自定义JS文件,使用上面的runJSPlugin直接写入到html中,在runJSPlugin 方法中 用到的js文件, 在项目中新建一个GROUP 统一管理,我觉得与www文件夹中的前端文件分离比较好。方法使用数组传递需要写入的JS的名称,这里列出Console.js

 
 
 
xxxxxxxxxx
 
 
 
 
var console = {
    log: function(message) { //console.log(msg);
        window.webkit.messageHandlers.wkbridge.postMessage({className: "Console", functionName: "log", data: {msg: message}});
    }
};
 

这样在js中只要执行 console.log() 就可以在Xcode控制台打印内容了。

  • Swift接口回调

js 层使用队列管理, 新建Base.js

 
 
 
xxxxxxxxxx
 
 
 
 
Queue = [];
function Task(id, callback, errorCallback) {
    var mTask = new Object;
    mTask.id = id;
    mTask.callback = callback;
    mTask.errorCallback = errorCallback;
    mTask.once = false;
    return mTask;
}
fireTask = function(i, j) {
    if (typeof Queue[i].callback == 'function') {
        Queue[i].callback(JSON.parse(j));
        if (Queue[i].once) Queue[i] = null;
    } 
};
onError = function (i, j) {
    Queue[i].errorCallback(j);
};
 
  • 实现获取沙盒根目录

    Sandbox.swift

 
 
 
x
 
 
 
 
import UIKit
class Sandbox: Plugin {
    
    func getRootPath() {
        let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let dic = NSDictionary.init(object: path.absoluteString, forKey: "rootPath" as NSCopying)
        _ = callback(values: dic)
    }
}
 

新建 Sandbox.js

 
 
 
xxxxxxxxxx
 
 
 
 
Sandbox = {
    getRootPath: function(onSuccess, onError) {
        Queue.push(Task(Queue.length, onSuccess, onError));
        window.webkit.messageHandlers.wkbridge.postMessage({className: "Finder", functionName: "getRootPath", taskId: Queue.length - 1});
    },
};
 

js中执行

 
 
 
xxxxxxxxxx
 
 
 
 
Sandbox.getRootPath(function(path){
    console.log(path.rootPath); //获取沙盒路径
})
 
  • Swift调用JS代码
 
 
 
xxxxxxxxxx
 
 
 
 
let js = "jsFunction(param);"
self.wk.evaluateJavaScript(js, completionHandler: nil)
 
  1. 访问沙盒文件
  • 将文件转换成base64的字符串传递给js, src直接设置data。
  • WKWebView 只能读取tmp中的文件,所以将文件写入到 NSTemporaryDirectory() 的路径下面就可以了,以后js需要访问什么文件,将文件复制到tmp文件夹就可以访问了,按需删除就好了。