前言
一直想系统的总结下UIWebView
和WKWebView
,这里整理了一个
Demo可供参考
分为两部分:
UIWebView & WKWebView 上
UIWebView & WKWebView 下
OC-->JS
UIWebView OC-->JS
- 1、通过调用
- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;
方法 - 2、在页面加载完成后,获取JSContext上下文,通过JSContext的
- (JSValue *)evaluateScript:(NSString *)script;
方法得到JSValue对象,JSValue对象可转为Array、Number、String、对象等数据类型
WKWebView OC-->JS
通过调用方法:- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id result, NSError * _Nullable error))completionHandler;
UIWebView OC-->JS解析
-
- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;
使用:
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
//@property (nonatomic, strong) UIWebView * webView;
self.title = [self.webView stringByEvaluatingJavaScriptFromString:@"document.title"];
}
1、该方法不能判断调用了一个js方法之后,是否发生了错误。当错误发生时,返回值为nil,而当调用一个方法本身没有返回值时,返回值也为nil,所以无法判断是否调用成功了。
2、返回值类型为nullable NSString *,就意味着当调用的js方法有返回值时,都以字符串返回,不够灵活。当返回值是一个js的Array时,还需要解析字符串,比较麻烦。
-
- (JSValue *)evaluateScript:(NSString *)script;
使用:
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
//@property (nonatomic, strong) UIWebView * webView;
//@property (nonatomic, strong) JSContext * jsContext;
//获取该UIWebview的javascript上下文
self.jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//JSContext oc调用js
JSValue *value = [self.jsContext evaluateScript:@"document.title"];
self.title = value.toString;
}
1、 其实WebKit都有一个内嵌的js环境,一般我们在页面加载完成之后,获取js上下文,然后通过JSContext的evaluateScript:
方法来获取返回值。因为该方法得到的是一个JSValue对象,所以支持JavaScript的Array、Number、String、对象等数据类型。该方法解决了stringByEvaluatingJavaScriptFromString:
返回值只是NSString的问题。
2、 [self.jsContext evaluateScript:@"document.titlexxxx"];
那么必然会报错,报错了,可以通过 @property (copy) void(^exceptionHandler)(JSContext *context, JSValue *exception);
,设置该block来获取异常
//在调用前,设置异常回调
[self.jsContext setExceptionHandler:^(JSContext *context, JSValue *exception){
NSLog(@"%@", exception);
}];
//执行方法
JSValue *value = [self.jsContext evaluateScript:@"document.titlexxxx"];
该方法,也很好的解决了stringByEvaluatingJavaScriptFromString:调用js方法后,出现错误却捕获不到的缺点。
WKWebView OC-->JS解析
//@property (strong, nonatomic) WKWebView *webView;
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
// self.title = self.webView.title;
// //执行一段js,并将结果返回,如果出错,error则不为空
[self.webView evaluateJavaScript:@"document.title" completionHandler:^(id _Nullable title, NSError * _Nullable error) {
self.title = title;
}];
}
该方法很好的解决了UIWebView
使用stringByEvaluatingJavaScriptFromString:
方法的两个缺点(1. 返回值只能是NSString。2. 报错无法捕获)。
JS-->0C
UIWebView JS-->0C
1、拦截URL
OC中,只要遵循了UIWebViewDelegate协议, 每次打开一个链接之前,都会触发方法- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
在该方法中,捕获该链接,并且返回NO(阻止本次跳转),从而执行对应的OC方法。
2、self.jsContext[@"yourMethodName"] = your block;
其中yourMethodName就是js的方法名称,赋给是一个block 里面是oc代码
WKWebView JS-->OC
1、URL拦截
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
2、WKUserContentController
中新增方法
- 注册回调
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
- js中调用方法
window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
- oc中将会收到
WKScriptMessageHandler
的回调
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
- 移除
- (void)removeScriptMessageHandlerForName:(NSString *)name;
拦截URL解析
1、Html
中实现
比如syhcandy://
方法是在html或者js中,点击某个按钮触发事件时,跳转到自定义URL Scheme构成的链接,而Objective-C中捕获该链接,从中解析必要的参数,实现JS到OC的一次交互。比如页面中一个a标签,链接如下:
<a href="syhcandy://smsLogin?username=syh&code=776632">短信验证登录</a>
UIWebView的JS-->OC中URL拦截实现
而在Objective-C中,只要遵循了UIWebViewDelegate
协议,那么每次打开一个链接之前,都会触发方法- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
在该方法中,捕获该链接,并且返回NO(阻止本次跳转),从而执行对应的OC方法。
#pragma mark - UIWebViewDelegate
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
//标准的URL包含scheme、host、port、path、query、fragment等
NSURL *URL = request.URL;
if ([URL.scheme isEqualToString:SHWebViewDemoScheme]) {
if ([URL.host isEqualToString:SHWebViewDemoHostSmsLogin]) {
NSLog(@"短信验证码登录,参数为 %@", URL.query); //短信验证码登录,参数为 username=syh&code=776632
return NO;
}
}
return YES;
}
缺点:无法直接获取本次交互的返回值,比较适合单向传参,且不关心回调的情景,比如h5页面跳转到native页面等。
WKWebView JS-->OC中URL拦截实现
当用户点击这个a标签时,会被拦截
//针对一次action来决定是否允许跳转,允许与否都需要调用decisionHandler,比如decisionHandler(WKNavigationActionPolicyCancel);
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
//可以通过navigationAction.navigationType获取跳转类型,如新链接、后退等
NSURL *URL = navigationAction.request.URL;
//判断URL是否符合自定义的URL Scheme
if([URL.scheme isEqualToString:SHWebViewDemoScheme]){
//根据不同的业务,来执行对应的操作,且获取参数
if([URL.host isEqualToString:SHWebViewDemoHostSmsLogin]){
NSString *param = URL.query;
NSLog(@"短信验证码登录, 参数为%@", param);//短信验证码登录, 参数为username=12323123&code=892845
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
}
decisionHandler(WKNavigationActionPolicyAllow);
}
html中定义了分享方法如下:
<div>
<a href="javascript:void(0);" class="sharebtn" onclick="share('分享标题', 'http://cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg', location.href)">分享活动,领30元红包</a>
</div>
<script>
//简单分享
function share (title, imgUrl, link) {
//便于WKWebView测试
window.webkit.messageHandlers.share.postMessage({title: title, imgUrl: imgUrl, link: link});
//这里需要OC实现
}
</script>
UIWebView JS-->OC中OC实现
self.jsContext[@"yourMethodName"] = your block;
方法映射
在页面加载完成时,先获取js上下文。获取到之后,我们就可以进行强大的方法映射了。
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
[self convertJSFunctionsToOCMethods];
}
- (void)convertJSFunctionsToOCMethods
{
//其中share就是js的方法名称,赋给是一个block 里面是iOS代码
//此方法最终将打印出所有接收到的参数,js参数是不固定的
self.jsContext[@"share"] = ^(){
NSArray *args = [JSContext currentArguments];//获取到share里的所有参数
//args中的元素是JSValue,需要转成OC的对象
NSMutableArray *messages = [NSMutableArray array];
for (JSValue *obj in args) {
[messages addObject:[obj toObject]];
}
NSLog(@"点击分享js传回的参数:\n%@", messages);
/**
点击分享js传回的参数:
(
"\U5206\U4eab\U6807\U9898",
"http://cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg",
"file:///Users/macair/Library/Developer/CoreSimulator/Devices/04C1A1B2-EBF1-4C3A-BC06-6664428718F6/data/Containers/Bundle/Application/2684F36D-58CB-4A37-96AB-334D21098682/WebViewDemo.app/test.html"
)
*/
};
}
scriptMessageHandler
WKUserContentController.h
新增两个方法如下:
//在OC中添加一个scriptMessageHandler,则会在all frames中添加一个js的function: window.webkit.messageHandlers.<name>.postMessage(<messageBody>) 。
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
- (void)removeScriptMessageHandlerForName:(NSString *)name;
WKWebView JS-->OC中OC实现
- 在OC中添加一个handler
//WKUserContentController *UserContentController = [[WKUserContentController alloc] init];
//注册回调
[UserContentController addScriptMessageHandler:self name:@"share"];
- js调用
share
方法后,OC会收到WKScriptMessageHandler
回调
#pragma mark - WKScriptMessageHandler js -> oc
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
if ([message.name isEqualToString:@"share"]) {
id body = message.body;
NSLog(@"share分享的内容为:%@", body);
/**
share分享的内容为:{
imgUrl = "http://cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg";
link = "file:///Users/macair/Library/Developer/CoreSimulator/Devices/04C1A1B2-EBF1-4C3A-BC06-6664428718F6/data/Containers/Bundle/Application/C009579D-10B9-49D2-A3A4-4D409157C158/WebViewDemo.app/test.html";
title = "\U5206\U4eab\U6807\U9898";
}
*/
}
}
- 记得移除注册的回调
- (void)dealloc
{
//记得移除
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"share"];
}
需求:写一个有两个参数,一个返回值的js方法,oc应该怎么替换呢?
Html中
<div onclick="alert(testAddMethod(1,5))">点击测试两数相加</div>
<script>
//该方法传入两个整数,求和,并返回结果
function testAddMethod (a, b) {
//需要OC实现a+b,并返回
return a + b;
}
</script>
UIWebView中对应的OC替换该方法的实现:
self.jsContext[@"testAddMethod"] = ^NSInteger(NSInteger a, NSInteger b){
return a + b;
// return a * b;
};
需求升级:调用方法原实现,并且在原结果上乘以10
//调用方法的本来实现,给原结果乘以10
JSValue *value = self.jsContext[@"testAddMethod"];
self.jsContext[@"testAddMethod"] = ^NSInteger(NSInteger a, NSInteger b){
JSValue *resultValue = [value callWithArguments:[JSContext currentArguments]];
return resultValue.toInt32 * 10;
};
新需求:h5中有一个分享按钮,用户点击之后,调用native分享(微信分享、微博分享等),在native分享成功或者失败时,回调h5页面,告诉其分享结果,h5页面刷新对应的UI,显示分享成功或者失败。
Html的代码
<a href="javascript:void(0);" onclick="test()">测试新分享</a></br>
<h>下面展示分享结果</p><div id="shareResult"></div>
<script>
/**
* 分享方法,并且会异步回调分享结果
* @param {对象类型} shareData 一个分享数据的对象,包含title,imgUrl,link以及一个回调function
* @return {void} 无同步返回值
js的shareNew方法的参数是一个对象,该对象包含了几个必要的字段,以及一个回调函数,这个回调函数有点像oc的block,调用者把一个function传入一个function当作参数,在适当时候,方法内实现者调用该function,实现对调用者的异步回调。
*/
function shareNew(shareData) {
var title = shareData.title;
var imgUrl = shareData.imgUrl;
var link = shareData.link;
var result = shareData.result;
//do something
//这里模拟异步操作
setTimeout(function(){
//2s之后,回调true分享成功
result(true);
}, 2000);
//用于WKWebView,因为WKWebView并没有办法把js function传递过去,因此需要特殊处理一下
//把js function转换为字符串,oc端调用时 (<js function string>)(true); 即可
shareData.result = result.toString();
window.webkit.messageHandlers.shareNew.postMessage(shareData);
}
function test() {
//清空分享结果
shareResult.innerHTML = "";
//调用时,应该
shareNew({
title: "title",
imgUrl: "http://img.dd.com/xxx.png",
link: location.href,
result: function(res) {
//这里shareResult 等同于 document.getElementById("shareResult")
shareResult.innerHTML = res ? "success" : "failure";
}
});
}
</script>
上述html代码解析:
点击之后,触发了test()
函数,test()
中封装了对shareNew()
函数的调用,且传了一个对象作为参数,对象中result字段对应的是个匿名函数,紧接着shareNew()
函数调用,其中的实现是2s过后,result(true)
;模拟js异步实现异步回调结果,分享成功。同时shareNew()
函数中,因为通过scriptMessageHandler
无法传递function
,所以先把shareData
对象中的result
这个匿名function
转成String,然后替换shareData
对象的result属性为这个String,并回传给OC,OC这边对应JS对象的数据类型是NSDictionary,我们打印并得到了所有参数,同时,把result字段对应的js function String取出来。这里我们延迟4s回调,模拟Native分享的异步过程,在4s后,也就是js中显示success的2s过后,调用js的匿名function,并传递参数(分享结果)。调用一个js function的方法是 functionName(argument);
,这里由于这个js的function已经是一个String了,所以我们调用时,需要加上(),如 (functionString)(argument);
因此,最终我们通过OC -> JS 的evaluateJavaScript:completionHandler:
方法,成功完成了异步回调,并传递给js一个分享失败的结果。
上面的描述看起来很复杂,其实就是先执行了JS的默认实现,后执行了OC的实现。上面的代码展示了如何解决scriptMessageHandler
的两个问题,并且实现了一个 JS -> OC、OC -> JS 完整的交互流程。
UIWebView在OC中的实现:
- (void)convertJSFunctionsToOCMethods
{
self.jsContext[@"shareNew"] = ^(JSValue *shareData){//首先这里要注意,回调的参数不能直接写NSDictionary类型,为何呢?
//仔细看,打印出的确实是一个NSDictionary,但是result字段对应的不是block而是一个NSDictionary
NSLog(@"%@", [shareData toObject]);
/**
{
imgUrl = "http://img.dd.com/xxx.png";
link = "file:///Users/macair/Library/Developer/CoreSimulator/Devices/04C1A1B2-EBF1-4C3A-BC06-6664428718F6/data/Containers/Bundle/Application/F0F701AB-D134-4596-8104-16EDD27CDBCD/WebViewDemo.app/test.html";
result = {
};
title = title;
}
*/
//获取shareData对象的result属性,这个JSValue对应的其实是一个javascript的function。
JSValue *resultFunction = [shareData valueForProperty:@"result"];
//回调block,将js的function转换为OC的block
void (^result)(BOOL) = ^(BOOL isSuccess) {
[resultFunction callWithArguments:@[@(isSuccess)]];
};
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
result(NO);
});
};
}
WKWebView在OC中的实现
- 注册回调
//WKUserContentController *UserContentController = [[WKUserContentController alloc] init];
//注册回调
[UserContentController addScriptMessageHandler:self name:@"shareNew"];
-
WKScriptMessageHandler
代理调用
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
if ([message.name isEqualToString:@"shareNew"]){
NSDictionary *shareData = message.body;
NSLog(@"%@分享的数据为: %@", message.name, shareData);
/**
shareNew分享的数据为: {
imgUrl = "http://img.dd.com/xxx.png";
link = "file:///Users/macair/Library/Developer/CoreSimulator/Devices/04C1A1B2-EBF1-4C3A-BC06-6664428718F6/data/Containers/Bundle/Application/9DE46251-970B-460B-AB13-86629C09C279/WebViewDemo.app/test.html";
result = "function (res) {\n //\U8fd9\U91ccshareResult \U7b49\U540c\U4e8e document.getElementById(\"shareResult\")\n shareResult.innerHTML = res ? \"success\" : \"failure\";\n\n }";
title = title;
}
*/
//模拟异步回调
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//读取js function的字符串
NSString *jsFunctionString = shareData[@"result"];
/*
function (res) {
//这里shareResult 等同于 document.getElementById("shareResult")
shareResult.innerHTML = res ? "success" : "failure";
}
*/
//拼接调用该方法的js字符串
NSString *callbackJs = [NSString stringWithFormat:@"(%@)(%d);", jsFunctionString, NO]; //后面的参数NO为模拟分享失败
//执行回调
[self.webView evaluateJavaScript:callbackJs completionHandler:^(id _Nullable result, NSError * _Nullable error) {
if (!error) {
NSLog(@"模拟回调,分享失败");
}
}];
});
}
}
- 移除注册
- (void)dealloc
{
//记得移除
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"shareNew"];
}
Native预览H5页面中的image
场景:在微信中浏览网页时,看到喜欢的图片,你会点击图片查看大图,然后长按图片保存。
分析
1、如果想在Native预览H5中的image,最需要的是什么?是图片的链接。如果能有缩略图更好了。
2、只要获取了链接,就可以跳转到一个ViewController中,预览图片,后续长按保存自然水到渠成。
3、那应该如何获取图片的链接呢?通过JS -> OC 传递图片url
方案
当页面加载完成后,给html页面中所有无默认点击事件的<img>添加点击事件,当用户点击时,拿到所有参数。
(其实这不是最好的方案,最好的解决方案是,跟前端约定一下,哪些图片需要预览,哪些img标签的id统一,或者有个特定的属性,这样客户端可以根据id找到这些img标签)
Html中有img标签
<img src="http://cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg">
写好一个ImgAddClickEvent.js
文件,来实现给所有无默认点击事件的<img>添加点击事件。
//获取所有img标签
var imgs = document.getElementsByTagName("img");
//获取所有的imgUrl
var imgUrls = new Array();
var x = 0;
var y = 0;
var width = 0;
var height = 0;
for (var i = 0; i < imgs.length; i++) {
var img = imgs[i];
//如果图片链接存在
if (img.src || img.getAttribute('data-src')) {
//添加到图片链接数组中
imgUrls.push(img.src || img.getAttribute('data-src'));
//如果图片没有默认的onclick事件,且父元素不是a标签,则添加onclick事件,当用户点击时,把图片链接回传给Native
if (!img.onclick && img.parentElement.tagName !== "A") {
//给图片添加下标的属性
img.index = i; //记录下标
//添加点击事件,并且回传选中的图片链接、下标、屏幕上的位置、全部的图片数组等
img.onclick = function() {
x = this.getBoundingClientRect().left;
y = this.getBoundingClientRect().top;
x = x + document.documentElement.scrollLeft;
y = y + document.documentElement.scrollTop;
width = this.width;
height = this.height;
var imgInfo = {
imgUrl: this.src || this.getAttribute('data-src'),
x: x,
y: y,
width: width,
height: height,
index: this.index,
imgUrls: imgUrls
};
//UIWebView使用
h5ImageDidClick(imgInfo);
}
}
}
}
function h5ImageDidClick (info){
//WKWebView使用
window.webkit.messageHandlers.imageDidClick.postMessage(info);
}
UIWebView实现
UIWebView
直接使用JavaScriptCore
给<img>
添加onclick
方法为OC的实现即可。
- (void)webViewDidFinishLoad:(UIWebView *)webView {
[self convertJSFunctionsToOCMethods];
}
- (void)convertJSFunctionsToOCMethods
{
//获取该UIWebview的javascript上下文
self.jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
/*
Native预览H5页面中的image
*/
//防止频繁IO操作,造成性能影响
static NSString *jsSource;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
jsSource = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"ImgAddClickEvent" ofType:@"js"] encoding:NSUTF8StringEncoding error:nil];
});
//先注入给图片添加点击事件的js
[self.jsContext evaluateScript:jsSource];
self.jsContext[@"h5ImageDidClick"] = ^(NSDictionary *imgInfo){
NSLog(@"UIWebView点击了html上的图片,信息是:%@", imgInfo);
};
/**
UIWebView点击了html上的图片,信息是:{
height = 168;
imgUrl = "http://cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg";
imgUrls = (
"http://cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg"
);
index = 0;
width = 252;
x = 8;
y = 8;
}
*/
}
WKWebView实现
添加脚本
/**
Native预览H5页面中的image,
页面中的所有img标签添加点击事件
*/
- (void)imgAddClickEvent
{
//防止频繁IO操作,造成性能影响
static NSString *jsSource;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
jsSource = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"ImgAddClickEvent" ofType:@"js"] encoding:NSUTF8StringEncoding error:nil];
});
/*
注入的js source可以是任何js字符串,也可以js文件。比如你有很多提供给h5使用的js方法,那么你本地可能就会有一个ImgAddClickEvent.js
*/
//添加自定义的脚本
WKUserScript *js = [[WKUserScript alloc] initWithSource:jsSource injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:NO];
[self.webView.configuration.userContentController addUserScript:js];
//注册回调
[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"imageDidClick"];
}
注册回调后的代理方法
#pragma mark - WKScriptMessageHandler js -> oc
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
if ([message.name isEqualToString:@"imageDidClick"]) {
//点击了html上的图片, Native预览H5页面中的image
NSLog(@"点击了html上的图片,参数为%@", message.body);
/**
点击了html上的图片,参数为{
height = 168;
imgUrl = "http://cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg";
imgUrls = (
"http://cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg"
);
index = 0;
width = 252;
x = 8;
y = 8;
}
注意这里的x,y是不包含自定义scrollView的contentInset的,如果要获取图片在屏幕上的位置:
x = x + self.webView.scrollView.contentInset.left;
y = y + self.webView.scrollView.contentInset.top;
*/
NSDictionary *dict = message.body;
NSString *selectedImageUrl = dict[@"imgUrl"];
CGFloat x = [dict[@"x"] floatValue] + + self.webView.scrollView.contentInset.left;
CGFloat y = [dict[@"y"] floatValue] + self.webView.scrollView.contentInset.top;
CGFloat width = [dict[@"width"] floatValue];
CGFloat height = [dict[@"height"] floatValue];
CGRect frame = CGRectMake(x, y, width, height);
NSUInteger index = [dict[@"index"] integerValue];
NSLog(@"点击了第%@个图片,\n链接为%@,\n在Screen中的绝对frame为%@,\n所有的图片数组为%@", @(index), selectedImageUrl, NSStringFromCGRect(frame), dict[@"imgUrls"]);
/*
点击了第0个图片,
链接为http://cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg,
在Screen中的绝对frame为{{8, 72}, {252, 168}},
所有的图片数组为(
"http://cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg"
)
*/
}
}
移除
- (void)dealloc
{
//记得移除
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"imageDidClick"];
}
Native为H5提供一套Native Api(微信、支付宝小程序)
很多时候,Native与H5交互得深了,必定会有一些更深层次的需求。比如h5想控制页面的pop、push、present,想调用Native的Share,想调用Native的扫描二维码功能,获取扫描结果……
微信提供的一些Api(扫码、选择照片等)都是如何实现的呢?很明显,native提供的。
首先,我们为H5提供一套Api,那自然Api是暴露给js的,所以这些Api也是js的。下面针对一些需求,分析下封装和实现。其中用到了js闭包,需要一点js知识。
从通讯录选择联系人
从通讯录选择联系人的例子,从js到native,再从native到js。
js端实现
/**
* Native为H5提供的Api接口
*
* @type {js对象}
*/
var DANativeApi = (function() {
var NativeApi = {
/**
* 从通讯录选择联系人
* @return {void} 无同步返回值,异步返回选择的结果
*/
choosePhoneContact: function(param) {
//具体是否需要判断
//调用native端
_nativeChoosePhoneContact(param);
}
}
//下面是一些私有函数
/**
* Native端实现选择联系人,并异步返回结果
* @param {[type]} param [description]
* @return {[type]} [description]
*/
function _nativeChoosePhoneContact(param) {
var callbackFunction = param.completion;
if (callbackFunction != undefined && callbackFunction != null && typeof(callbackFunction) === "function") {
param.completion = callbackFunction.toString();
}
//js -> oc
window.webkit.messageHandlers.nativeChoosePhoneContact.postMessage(param);
}
//闭包,把Api对象返回
return NativeApi;
})();
/*
//选择联系人
DANativeApi.choosePhoneContact({
completion: function(res) {
alert("选择联系人的结果为:" + JSON.stringify(res));
}
});
*/
Html中调用
<div>
<a href="javascript:void(0);" onclick="chooseContact()">选择联系人</a>
<div id="contactInfo"></div>
</div>
<script>
function chooseContact() {
DANativeApi.choosePhoneContact({
completion: function(res) {
contactInfo.innerHTML = JSON.stringify(res);
}
});
}
</script>
WKWebView实现
- 添加脚本并注册回调
/**
添加native端的api
*/
- (void)addNativeApiToJS
{
//防止频繁IO操作,造成性能影响
static NSString *nativejsSource;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
nativejsSource = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"NativeApi" ofType:@"js"] encoding:NSUTF8StringEncoding error:nil];
});
//添加自定义的脚本
WKUserScript *js = [[WKUserScript alloc] initWithSource:nativejsSource injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[self.webView.configuration.userContentController addUserScript:js];
//注册回调
[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"nativeChoosePhoneContact"];
}
-
WKScriptMessageHandler
回调代理方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
if ([message.name isEqualToString:@"nativeChoosePhoneContact"]) {
NSLog(@"正在选择联系人");
[self selectContactCompletion:^(NSString *name, NSString *phone) {
NSLog(@"选择完成");
//读取js function的字符串
NSString *jsFunctionString = message.body[@"completion"];
//拼接调用该方法的js字符串
NSString *callbackJs = [NSString stringWithFormat:@"(%@)({name: '%@', mobile: '%@'});", jsFunctionString, name, phone];
//执行回调
[self.webView evaluateJavaScript:callbackJs completionHandler:^(id _Nullable result, NSError * _Nullable error) {
}];
}];
}
}
- 获取联系方式的oc实现
//<CNContactPickerDelegate>
//@property (nonatomic, copy) void(^completion)(NSString *name, NSString *phone);
#pragma mark 选择联系人
- (void)selectContactCompletion:(void(^)(NSString *name, NSString *phone))completion
{
self.completion = completion;
CNContactPickerViewController *picker = [[CNContactPickerViewController alloc] init];
picker.delegate = self;
picker.displayedPropertyKeys = @[CNContactPhoneNumbersKey];
[self presentViewController:picker animated:YES completion:^{
}];
}
#pragma mark - CNContactPickerDelegate
- (void)contactPicker:(CNContactPickerViewController *)picker didSelectContactProperty:(CNContactProperty *)contactProperty
{
if (![contactProperty.key isEqualToString:CNContactPhoneNumbersKey]) {
return;
}
CNContact *contact = contactProperty.contact;
NSString *name = [CNContactFormatter stringFromContact:contact style:CNContactFormatterStyleFullName];
CNPhoneNumber *phoneNumber = contactProperty.value;
NSString *phone = phoneNumber.stringValue.length ? phoneNumber.stringValue : @"";
//可以把-、+86、空格这些过滤掉
NSString *phoneStr = [phone stringByReplacingOccurrencesOfString:@"-" withString:@""];
phoneStr = [phoneStr stringByReplacingOccurrencesOfString:@"+86" withString:@""];
phoneStr = [phoneStr stringByReplacingOccurrencesOfString:@" " withString:@""];
phoneStr = [[phoneStr componentsSeparatedByCharactersInSet:[[NSCharacterSet characterSetWithCharactersInString:@"0123456789"] invertedSet]] componentsJoinedByString:@""];
//回调
if (self.completion) {
self.completion(name, phoneStr);
}
//dissMiss
[picker dismissViewControllerAnimated:YES completion:nil];
}
- (void)contactPickerDidCancel:(CNContactPickerViewController *)picker
{
[picker dismissViewControllerAnimated:YES completion:nil];
}
- 移除注册的回调
- (void)dealloc
{
//记得移除
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"nativeChoosePhoneContact"];
}