最近公司要使用Hybird混合开发,所以就要学习一下JS与Swift的交互,以便之后的工作;据我所知,iOS下JS与原生的交互有很多种具体有:
- 使用UIWebView与WKWebView的代理方法,在JS 中做一次URL跳转,然后在Swift中拦截跳转
- 使用WKWebView 的MessageHandler
- 使用系统库JavaScriptCore,来做相互调用(iOS 7推出的)
- 使用第三方库WebViewJavascriptBridge
- 使用第三方cordova库,以前叫PhoneGap(这是一个库平台的库)
- 使用React Native
本文主要是 写使用UIWebView与WKWebView的代理方法,在JS 中做一次URL跳转,然后在Swift中拦截跳转
的情况
1. 使用的情景
当我们在与JS交互的接口比较少时,就适用这种情况
2. UIWebView的情景
首先,创建UIWebView,并加载本地HTML
lazy var webView: UIWebView = {[unowned self] in
let view = UIWebView(frame: self.view.bounds)
let htmlURL = Bundle.main.url(forResource: "anran.html", withExtension: nil)
let request = URLRequest(url: htmlURL!)
let request1 = URLRequest(url: URL(string: "https://www.baidu.com")!)
view.scrollView.decelerationRate = UIScrollViewDecelerationRateNormal
view.loadRequest(request)
view.scrollView.bounces = false
view.delegate = self
return view
}()
然后,在HTML里,定义按钮,来触发调用原生的方法,然后再将执行结果回调到js 里。
<input type="button" value="点我点我" onclick="scanClick()" />
<input type="button" value="获取定位" onclick="locationClick()" />
<input type="button" value="修改导航栏颜色" onclick="colorClick()" />
<input type="button" value="分享" onclick="shareClick()" />
<input type="button" value="支付" onclick="payClick()" />
<input type="button" value="动次打次" onclick="shake()" />
<input type="button" value="返回" onclick="goBack()" />
function loadURL(url) {
var iFrame;
iFrame = document.createElement("iframe");
iFrame.setAttribute("src", url);
iFrame.setAttribute("style", "display:none;");
iFrame.setAttribute("height", "0px");
iFrame.setAttribute("width", "0px");
iFrame.setAttribute("frameborder", "0");
document.body.appendChild(iFrame);
// 发起请求后这个iFrame就没用了,所以把它从dom上移除掉
iFrame.parentNode.removeChild(iFrame);
iFrame = null;
}
1.为什么自定义一个loadURL 方法,不直接使用window.location.href?
答:因为如果当前网页正使用window.location.href加载网页的同时,调用window.location.href去调用OC原生方法,会导致加载网页的操作被取消掉。
同样的,如果连续使用window.location.href执行两次OC原生调用,也有可能导致第一次的操作被取消掉。所以我们使用自定义的loadURL,来避免这个问题。
loadURL的实现来自关于UIWebView和PhoneGap的总结一文。
2.为什么loadURL 中的链接,使用统一的scheme?
答:便于在OC 中做拦截处理,减少在JS中调用一些OC 没有实现的方法时,webView 做跳转。因为我在OC 中拦截URL 时,根据scheme (即haleyAction)来区分是调用原生的方法还是正常的网页跳转。然后根据host(即//后的部分getLocation)来区分执行什么操作。
3.为什么自定义一个asyncAlert方法?
答:因为有的JS调用是需要OC 返回结果到JS的。stringByEvaluatingJavaScriptFromString是一个同步方法,会等待js 方法执行完成,而弹出的alert 也会阻塞界面等待用户响应,所以他们可能会造成死锁。导致alert 卡死界面。如果回调的JS 是一个耗时的操作,那么建议将耗时的操作也放入setTimeout的function 中。
最后,拦截URL也就是自定义的协议,UIWebView 有一个代理方法,可以拦截到每一个链接的Request。return true,webView 就会加载这个链接;return false,webView 就不会加载这个连接,我们就在这个拦截的代理方法中处理自己的URL
func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
let url = request.url
let scheme = url?.scheme
if let URL = url, scheme == "anranaction" {
self.handleCustomAction(url: URL)
return false
}
return true
}
这里通过scheme,来拦截掉自定义的URL 就非常容易了,如果不同的方法使用不同的scheme,那么判断起来就非常的麻烦,看看我们是怎么处理的
// MARK: - 处理URL然后调用方法
func handleCustomAction(url: URL) {
let host = url.host
if host == "scanClick" {
} else if host == "shareClick" {
share(url: url)
} else if host == "getLocation" {
getLocation()
} else if host == "setColor" {
ChangeColor(url: url)
} else if host == "payAction" {
payAction(url: url)
} else if host == "shake" {
sharedAction()
} else if host == "back" {
goBack()
}
}
我们在JS中调用Swift 方法的时候,也需要传参数到Swift 中,怎么传呢?
就像一个get 请求一样,把参数放在后面:
function shareClick() {
loadURL("haleyAction://shareClick?title=标题&content=内容 &url=http://www.baidu.com");
}
我们如何获取参数,并且将参数传回JS中,所有的参数都在URL的query中,先通过&将字符串拆分,在通过=把参数拆分成key 和实际的值
func share(url: URL) {
guard let params = url.query?.components(separatedBy: "&") else { return }
var tempDic = [String:Any]()
for paramStr in params {
let dicArray = paramStr.components(separatedBy: "=")
if dicArray.count > 1 {
guard let str = dicArray[1].removingPercentEncoding else { return }
tempDic[dicArray[0]] = str
}
}
let title = tempDic["title"]
let content = tempDic["content"]
let url = tempDic["url"]
let jsStr = "shareResult('\(title ?? "")','\(content ?? "")','\(url ?? "")')"
webView.stringByEvaluatingJavaScript(from: jsStr)
}
Swift调用JS
let jsStr = "setLocation('\("杭州市拱墅区下沙中国计量学院")')"
webView.stringByEvaluatingJavaScript(from: jsStr)
Swift中可以往HMTL的JS环境中插入全局变量、JS方法
func webViewDidFinishLoad(_ webView: UIWebView) {
print("webView加载完成然后调用")
webView.stringByEvaluatingJavaScript(from: "var arr = [3, 4, 'abc']")
}
3. WKWebView的情景
由于UIWebView比较耗内存,性能上不太好,而苹果在iOS 8中推出了WKWebView。
同样的用WKWebView也可以拦截URL,做JS 与Native交互
安然 | 打开百度网页前 | 打开百度网页后 |
---|---|---|
UIWebView | 内存47M | 内存75.6M,最高峰83M |
WKWebView | 内存47M | 内存51M |
尽管WKWebView有很多的优点但是也有很多的缺点,比如他的储存模式,是封闭的,我们要访问也是不容易的,这个问题在以后我专门的学习一下,这篇就不在解释了
WKWebView 与 UIWebView 拦截URL 的处理方式基本一样。除了代理方法和WKWebView的使用不太一样,关于WKWebView更详尽的讲解和用法,还是自行搜索学习,本文重点还是讲解如何实现JS 与Native互相调用
首先, 创建WKWebView,WKWebView的创建有几点不同:
- 初始化configuration参数,当然这个参数我们也可以不传,直接使用默认的设置就好
- WKWebView的代理有两个navigationDelegate和UIDelegate。我们要拦截URL,就要通过navigationDelegate的一个代理方法来实现。如果在HTML中要使用alert等弹窗,就必须得实现UIDelegate的相应代理方法
- 在iOS 9之前,WKWebView加载本地HTML会有一些问题(不能加载本地HTML,或者部分CSS/本地图片加载不了等)
lazy var webView: WKWebView = {[unowned self] in
let configuration = WKWebViewConfiguration()
configuration.userContentController = WKUserContentController()
let preferences = WKPreferences()
preferences.javaScriptCanOpenWindowsAutomatically = true
preferences.minimumFontSize = 30.0
configuration.preferences = preferences
let view = WKWebView(frame: self.view.frame, configuration: configuration)
let urlStr = Bundle.main.path(forResource: "anran.html", ofType: nil)
let fileURL = URL(fileURLWithPath: urlStr!)
view.loadFileURL(fileURL, allowingReadAccessTo: fileURL)
view.navigationDelegate = self
view.uiDelegate = self
return view
}()
然后,使用WKNavigationDelegate中的代理方法,拦截自定义的URL来实现JS调用OC方法
- 如果实现了这个代理方法,就必须得调用decisionHandler这个block,否则会导致app 崩溃。block参数是个枚举类型,WKNavigationActionPolicyCancel代表取消加载,相当于UIWebView的代理方法return false的情况;WKNavigationActionPolicyAllow代表允许加载,相当于UIWebView的代理方法中 return true的情况
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
let url = navigationAction.request.url
let scheme = url?.scheme
if let URL = url, scheme == "anranaction" {
self.handleCustomAction(url: URL)
// 一定要实现否则会崩溃
decisionHandler(.cancel)
return
}
decisionHandler(.allow)
}
最后,JS 调用Native方法后,有的操作可能需要将结果返回给JS。这时候就是Native 调用JS 方法的场景,WKWebView 提供了一个新的方法evaluateJavaScript:completionHandler:实现Native调用JS 等场景
func getLocation() {
let jsStr = "setLocation('\("杭州市拱墅区下沙中国计量学院")')"
webView.evaluateJavaScript(jsStr) { (result, error) in
print("\(result)")
}
}
这个方法evaluateJavaScript(<#T##javaScriptString: String##String#>, completionHandler: <#T##((Any?, Error?) -> Void)?##((Any?, Error?) -> Void)?##(Any?, Error?) -> Void#>)
没有返回值,JS 执行成功还是失败会在completionHandler 中返回。所以使用这个API 就可以避免执行耗时的JS,或者alert 导致界面卡住的问题
WKWebView中使用弹窗
在上面提到,如果在WKWebView中使用alert、confirm 等弹窗,就得实现WKWebView的WKUIDelegate中相应的代理方法。
如果,我在JS中要显示alert 弹窗,就必须实现如下代理方法,否则alert 并不会弹出
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
let alert = UIAlertController(title: "提醒", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "知道", style: .cancel, handler: { (action) in
completionHandler()
}))
self.present(alert, animated: true, completion: nil)
}
其中completionHandler
这个block 必须调用