iOS下JS与Swift互相调用(一)UIWebView拦截URL

iOS 原生与 JavaScript 的交互方式很多,目前我知道的大概有以下几种:

  • 在JS 中做一次URL跳转,然后再拦截跳转(这里分为UIWebView 和 WKWebView两种)

  • 利用WKWebView 的MessageHandler

  • 利用系统库JavaScriptCore(iOS7 推出的)

  • 利用第三方库WebViewJavascriptBridge

  • 利用第三方cordova库,以前叫PhoneGap

  • 现在最热门的React Native

这篇文章主要是介绍 UIWebView 拦截 URL 的方式与 JS 交互,如果 iOS 与原生的交互比较少或者简单,完全可以使用这种方式

1. 创建UIWebView,并加载本地HTML

为了测试方便,这里是从本地加载HTML文件,真实情况肯定是从网络加载HTML网页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func setupWebView() {
self.webView = UIWebView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height))
self.webView?.delegate = self
// 取消不想要webView 的回弹效果
self.webView?.scrollView.bounces = false
// UIWebView 滚动的比较慢,这里设置为正常速度
self.webView?.scrollView.decelerationRate = UIScrollViewDecelerationRateNormal
let url = Bundle.main.url(forResource: "index.html", withExtension: nil)
let request = NSURLRequest(url: url!)
self.webView?.loadRequest(request as URLRequest)
self.view.addSubview(self.webView!)
}

本地的HTML文件里面定义了一些按钮,来触发调用原生的方法,然后再将执行结果回调到JS里

1
2
3
4
5
6
7
8
<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()" />
<input type="button" value="百度" src = "https://www.badu.com" />

下面是部分JS代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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;
}
function scanClick() {
alert(arr);
loadURL("haleyAction://scanClick");
}
function shareClick() {
loadURL("haleyAction://shareClick?title=测试分享的标题&content=测试分享的内容&url=http://www.baidu.com");
}
function locationClick() {
loadURL("haleyAction://getLocation");
}

JS代码解释:

  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 中

2. 拦截URL

在UIWebView的代理方法,可以拦截到每一个链接的Request,返回 true,webView 就会加载这个链接;返回 false,webView 就不会加载这个链接,我们就在这个拦截URL的代理方法中通过 scheme 处理自己定义的URL

1
2
3
4
5
6
7
8
9
//MARK: - UIWebViewDelegate
func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
let url = request.url
if url?.scheme == "haleyaction" {
handleCustomAction(url: url!)
return false
}
return true
}

然后根据不同 host 执行不同的 Native 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func handleCustomAction(url:URL) {
let host = url.host!
switch host {
case "scanClick":
print("saoyisao")
case "shareClick":
share(url: url)
case "getLocation":
getLocation()
case "setColor":
changeBackGroundColor(url: url)
case "payAction":
payAction(url: url)
case "shake":
sharkeAction()
case "back":
goBack()
default:
break
}
}

3. JS和Swift的相互调用

JS调用Swift的原生方法,并且将结果返回给JS:

1
2
3
4
5
6
7
//将swift的内容回调到JS中
func getLocation() {
//获取位置信息
//将获取的位置信息回调到js
let jsStr = "setLocation('广东省深圳市南山区')"
self.webView?.stringByEvaluatingJavaScript(from: jsStr)
}

有时候我们在JS中调用Swift方法的时候,也需要传参数到Swift中,怎么传呢?JS自定义的URL就像一个 get 请求一样,把参数放在后面:

1
2
3
function shareClick() {
loadURL("haleyAction://shareClick?title=测试分享的标题&content=测试分享的内容&url=http://www.baidu.com");
}

所有的参数都在URL的query中,获取这些参数需要先通过&将字符串拆分,在通过=把参数拆分成key 和实际的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//用js把参数传给swift
func share(url: URL) {
let params = url.query?.removingPercentEncoding?.components(separatedBy: "&")
var paramDic = [String: String]()
for paramStr in params! {
let arr = paramStr.components(separatedBy: "=")
if arr.count > 1 {
paramDic[arr[0]] = String.init(arr[1].utf8)
}
}
let title = paramDic["title"] ?? ""
let content = paramDic["content"] ?? ""
let shareUrl = paramDic["url"] ?? ""
// 在这里执行分享的操作
// 将分享结果返回给js
let jsStr = "shareResult('\(title)','\(content)','\(shareUrl)')"
self.webView?.stringByEvaluatingJavaScript(from: jsStr)
}

3. Swift调用JS注意

将Swift执行结果返回给JS 需要注意:

如果回调执行的JS 方法带参数,而参数不是字符串时,不要加单引号,否则可能导致调用JS 方法失败。比如我这样的:

另外,利用webView.stringByEvaluatingJavaScript(from: "var arr = [3, 4, 'abc']"),可以往HMTL的JS环境中插入全局变量、JS方法等

原文地址: iOS下 JS 与 OC 互相调用(一)–UIWebView 拦截URL

原文是讨论 OC 与 JS 的交互,我按照作者的思路,用 Swift 实现 Native 与 JS 的交互,这是 代码地址

本人刚开始写博客,主要是为了给自己的知识点做一个笔记,方便自己以后查阅,如果能让别人有所启发也是荣幸之至!如有错误,欢迎指正!