iOS中JS和OC的交互

javascript

在ios的开发中,会经常的使用到webview,而这是两套不同的系统,如果需要相互进行数据、控制的传递,那就需要Javascript了,这就是我们今天所说的Javascript和OC交互的由来。
由于技术的不断更新,所以在不同的时期都有不同的解决方案,比如现在还大名鼎鼎的WebViewJavascriptBridge

在此之前,我们先来写一个html

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
27
28
29
<html>
<head>
<style>
<!-- CSS部分就不写了,省点空间,详细的还是看代码吧。 -->
</style>
<script>
function button_click(){
var input = document.getElementById('i_text');
// 这里是主要测试各种方法的地方,其他的都一样
}
function oc_transferValue_to_js(v) {
document.getElementById('p_log_oc').innerHTML = '从OC中传递过来的数据是:<br>' + v
return 'byebye'
}
</script>
</head>
<body>
<div class="o_frame">
<input type="text" id="i_text" />
<br/>
<button id="b_test" onclick="button_click()">点击测试</button>
</div>
<br />
<div class="o_frame">
<p>从OC中传递过来的内容</p>
<p id="p_log_oc"></p>
</div>
</body>
</html>

在各种实现方法中,都大同小异,在html中的差别主要是button_click方法中,JS往OC中传递参数的方式不同。
以下各种实现中,如未做特殊说明,则JS与OC通信的各种方法都需要网页端和iOS端进行交流确认以后规定的方法、传参名,以保证其有效性。


iOS 7以前的UIWebView

JS to OC

这时候,主要用的还是UIWebView,实际上iOS8以前都主要是它。
在查看UIWebView的SDK的时候,都知道他有一个代理方法:

1
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;

这个方法的作用,主要是在网页加载过程中,每次加载新的url请求,都会调用的一个代理方法,允许我们对即将加载的链接进行一些控制处理。
所以,HTML中传参给OC,方法就呼之欲出了,只需要在button_click的时候这样:

1
window.location.href = 'ios_cmd://' + input.value;

然后在这个代理中,从请求的URL中截取ios_cmd://字符串就行了:

1
2
3
4
5
NSString *urlstr = request.URL.absoluteString;
NSRange cmd_range = [urlstr rangeOfString:@"ios_cmd://"];
if (cmd_range.location != NSNotFound) {
[self appendValue:[@"从JS中传递过来的值:\n" stringByAppendingString:[urlstr substringFromIndex:cmd_range.location + cmd_range.length]]];
}

OC to JS

而OC往html中传参,可以用webview的自带的方法,如:

1
2
3
NSString *jsstr = [NSString stringWithFormat:@"oc_transferValue_to_js('%@')", self.textfield.text];
NSString *rev = [self.webView stringByEvaluatingJavaScriptFromString:jsstr];
[self appendValue:[NSString stringWithFormat:@"OC调用JS后的返回值:%@", rev]];

这个方法是有返回值的,能够在往JS中传参的同时接收返回值。


iOS 7以后的UIWebView

苹果在iOS7的时候,推出了JavascriptCore,目的就是为了能够让OC与JS更为友好的交互,这里说的JS可以是本地的纯JS脚本,也可以是从html中拿出的JS对象。

JavascriptCore中,操作的主要对象是JSContext,对于纯JS脚本可以这样做:

1
2
3
4
5
NSString *jsPath = [[NSBundle mainBundle] pathForResource:@"t" ofType:@"js"];
NSString *jscript = [NSString stringWithContentsOfFile:jsPath encoding:NSUTF8StringEncoding error:nil];
JSContext *context = [[JSContext alloc] init];
// 把JS文件的内容加进context
[context evaluateScript:jscript];

对于UIWebView可以这样来获取,不过需要在- (void)webViewDidFinishLoad:(UIWebView *)webView以后:

1
self.context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

只要拿到JSContext,然后就能做很多事了。

JS to OC

JS传参给OC,需要在OC里这么做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
self.context[@"js_transfer_to_oc"] = ^(NSString *jsv){
NSMutableString *res = [[NSMutableString alloc] init];
// 直接获取参数,在block中可以添加参数,然后直接使用
[res appendFormat:@"直接读取的参数:\n%@", jsv];
[res appendString:@"\n\n使用argument读取传参:\n"];
// 也能通过JSContext去获取参数列表,注意,这时候获取的是一个数组
NSArray *args = [JSContext currentArguments];
for (NSString *v in args) {
[res appendFormat:@"%@\n", v];
}
[weakSelf appendValue:res];
};

然后在JS中直接调用就行了 : js_transfer_to_oc(input.value);

如果想要实现多个方法,只需要使用context注册多个block的监听就是,一定要注意方法名保持一致 **。

OC to JS

这就简单了,直接使用Context调用JS中的方法就行了:

1
2
3
4
5
6
7
JSValue *my_jsc = self.context[@"oc_transferValue_to_js"];
JSValue *rev = [my_jsc callWithArguments:@[self.textfield.text]];
// 也能直接调用
// JSValue *rev = [self.context evaluateScript:[NSString stringWithFormat:@"oc_transferValue_to_js('%@')", self.textfield.text]];
if (rev) {
[self appendValue:[NSString stringWithFormat:@"OC调用JS后的返回值:%@", [rev toString]]];
}

iOS 8以后的WebKit

iOS 8以后,苹果推出了新的WebView框架,用来替代以往臃肿的UIWebView,在本次更新中,也提供了大量的实用api,可喜的是,苹果也提供了新的JS与OC交互的方式,真的很方便。

JS to OC

首先需要在WKWebView初始化的时候,注册一下方法,如下:

1
2
3
4
5
6
7
WKUserContentController *userController = [[WKUserContentController alloc] init];
[userController addScriptMessageHandler:self name:@"oc_method"];
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
configuration.userContentController = userController;
self.webview = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];

然后在实现WKScriptMessageHandler的代理方法中就能收到JS传来的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
SEL selc = NSSelectorFromString([message.name stringByAppendingString:@":"]);
if ([self respondsToSelector:selc]) {
[self performSelector:selc withObject:message.body];
} else {
NSLog(@"方法没实现");
}
}
- (void)oc_method:(NSString *)arg
{
[self appendValue:[NSString stringWithFormat:@"从JS中传递过来的数据:\n%@", arg]];
}

所有在WKUserContentController中注册的方法,都会在这个代理方法中以WKScriptMessage的方式获得JS中传递过来的信息。
这个name可以直接当做一个方法名来用,当然,需要在本地实现;或者说只作为一个方法对应的key,然后对应着本地的一个方法实现也行。
这个想怎么实现就看自己需求了。

然后在JS中需要这样调用:

1
window.webkit.messageHandlers.oc_method.postMessage(input.value);

注意其中的方法名,要跟在OC中注册的保持一致。

OC to JS

可以直接用webview调用就行:

1
2
3
4
5
6
__weak AUUOriginalWKWebViewController *weakSelf = self;
[self.webview evaluateJavaScript:[NSString stringWithFormat:@"oc_transferValue_to_js('%@')", self.textfield.text] completionHandler:^(id _Nullable value, NSError * _Nullable error) {
if (!error) {
[weakSelf appendValue:[NSString stringWithFormat:@"OC调用JS后回传的数据:\n%@", value]];
}
}];

总结

UIWebView现在也能用,但是技术不是在不断的创新发展么,虽然苹果不会说直接就废弃掉旧的API,但是WKWebView也是一种趋势嘛,就看各自需要兼容到的版本了。
对于交互上来说,实现的方法其实也很简单,一定要注意的是:

  • 方法名要写对
  • 方法名要对的上

附 : 项目Demo