iOS之OC与JS交互(WKWebView)

前言

最近项目开发中用到了OC与JS交互方面的知识,以前也用过UIWebView JS与OC交互方面的,使用的苹果在iOS7开放的javascriptCore框架,使用起来挺方便快捷,javascriptCore源码是开放的,有兴趣的可以去了解一下。

自从iOS8,苹果就UIWebView性能不好,推出了WKWebView,以及github上评分很高的WebViewJavascriptBridge里面最新版本也最WKWebView做了兼容。

实现的方式

所以我总结的方式,分UIWebview、WKWebView、以及通用版本的第三方WebViewJavascriptBridge,进行实现。

  • UIWebView中JS与OC交互
  • WKWebView中JS与OC交互(只能iOS8及之后的版本)
  • WebViewJavascriptBridge对UIWebView与WKWebView做了同意处理。

WKWebView出现背景和优点

UIWebView自iOS2就有,WKWebView从iOS8才有,毫无疑问WKWebView将逐步取代笨重的UIWebView。

通过简单的测试即可发现UIWebView占用过多内存,且内存峰值更是夸张。WKWebView网页加载速度也有提升,但是并不像内存那样提升那么多

下面列举一些其它的优势:

  • 更多的支持HTML5的特性

  • 官方宣称的高达60fps的滚动刷新率以及内置手势

  • Safari相同的JavaScript引擎

  • 将UIWebViewDelegate与UIWebView拆分成了14类与3个协议(官方文档说明)

  • 另外用的比较多的,增加加载进度属性:estimatedProgress

了解WKWebView要涉及一些类

首页使用WKWebView要引进:

#import <WebKit/WebKit.h>
  • WKWebView使用
//初始化
self.webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT) configuration:config];

// UI代理
self.webView.UIDelegate = self;

// 导航代理        self.webView.navigationDelegate = self;

// 是否允许手势左滑返回上一级, 类似导航控制的左滑返回   self.webView.allowsBackForwardNavigationGestures = YES;

//可返回的页面列表, 存储已打开过的网页 
WKBackForwardList * backForwardList = [self.webView backForwardList];

 //页面后退
 [self.webView goBack];
 
 //页面前进
 [self.webView goForward];
 
 //刷新当前页面
 [self.webView reload];
 
 //加载本地的html
 NSString *path = [[[NSBundle mainBundle] bundlePath]  stringByAppendingPathComponent:@"test.html"];
 
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL fileURLWithPath:path]];
[self.webView loadRequest:request];
    
  • WKWebViewConfiguration:为添加WKWebView配置信息
//创建网页配置对象
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
        
// 创建设置对象
WKPreferences *preference = [[WKPreferences alloc]init];

//最小字体大小 当将javaScriptEnabled属性设置为NO时,可以看到明显的效果
preference.minimumFontSize = 0;

//设置是否支持javaScript 默认是支持的
preference.javaScriptEnabled = YES;

// 在iOS上默认为NO,表示是否允许不经过用户交互由javaScript自动打开窗口        
preference.javaScriptCanOpenWindowsAutomatically = YES;
config.preferences = preference;
        
// 是使用h5的视频播放器在线播放, 还是使用原生播放器全屏播放
config.allowsInlineMediaPlayback = YES;
        
//设置视频是否需要用户手动播放  设置为NO则会允许自动播放
config.requiresUserActionForMediaPlayback = YES;
       
//设置是否允许画中画技术 在特定设备上有效
config.allowsPictureInPictureMediaPlayback = YES;

//设置请求的User-Agent信息中应用程序名称 iOS9后可用
config.applicationNameForUserAgent = @"ChinaDailyForiPad";

//自定义的WKScriptMessageHandler 是为了解决内存不释放的问题
WeakWebViewScriptMessageDelegate *weakScriptMessageDelegate = [[WeakWebViewScriptMessageDelegate alloc] initWithDelegate:self];

//这个类主要用来做native与JavaScript的交互管理
WKUserContentController * wkUController = [[WKUserContentController alloc] init];

//注册一个name为jsToOcNoPrams的js方法
[wkUController addScriptMessageHandler:weakScriptMessageDelegate name:@"js调用OC的方法"];

[wkUController addScriptMessageHandler:weakScriptMessageDelegate name:@"js调用OC的方法"]; 

config.userContentController = wkUController;
       
  • WKUserScript:用于进行JavaScript注入
//以下代码适配文本大小,由UIWebView换为WKWebView后,会发现字体小了很多,这应该是WKWebView与html的兼容问题,解决办法是修改原网页,要么我们手动注入JS

NSString *jSString = @"var meta = document.createElement('meta'); 
meta.setAttribute('name', 'viewport'); 
meta.setAttribute('content', 
'width=device-width'); 
document.getElementsByTagName('head')
[0].appendChild(meta);";

//用于进行JavaScript注入
WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jSString 
injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];

[config.userContentController addUserScript:wkUScript];

  • WKUserContentController:这个类主要用来做native与JavaScript的交互管理
 //这个类主要用来做native与JavaScript的交互管理
WKUserContentController * wkUController = [[WKUserContentController alloc] init];

//注册一个name为jsToOcNoPrams的js方法,设置处理接收JS方法的代理
[wkUController addScriptMessageHandler:self  name:@"jsCallOCNoPrams"];
[wkUController addScriptMessageHandler:self  name:@"jsCallOCNoPrams"];
config.userContentController = wkUController;

//用完记得移除
//移除注册的js方法,避免内存泄露
[[self.webView configuration].userContentController 
removeScriptMessageHandlerForName:@"jsToOcNoPrams"];

[[self.webView configuration].userContentController 
removeScriptMessageHandlerForName:@"jsToOcWithPrams"];

  • WKScriptMessageHandler:这个协议类专门用来处理监听JavaScript方法从而调用原生OC方法,和WKUserContentController搭配使用。
注意:遵守WKScriptMessageHandler协议,代理是由WKUserContentControl设置

//通过接收JS传出消息的name进行捕捉的回调方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {

    /*
     message.body: 触发js方法传的值
     message.name: 触发js的方法名
    */
    NSLog(@"传的值L: --%@ \n 方法名: --- %@",message.body, message.name);
}

  • WKNavigationDelegate :主要处理一些跳转、加载处理操作
// 页面开始加载时调用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
}

// 页面加载失败时调用
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error {
   
} 

// 当内容开始返回时调用
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
}

// 页面加载完成之后调用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
   
}

//提交发生错误时调用
- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error {
    
}  

// 接收到服务器跳转请求即服务重定向时之后调用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation {
}

// 根据WebView对于即将跳转的HTTP请求头信息和相关信息来决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    
}
    
// 根据客户端受到的服务器响应头以及response相关信息来决定是否可以跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {

    NSString * urlStr = navigationResponse.response.URL.absoluteString;
    NSLog(@"当前跳转地址:%@",urlStr);
    //允许跳转    
    decisionHandler(WKNavigationResponsePolicyAllow);
    
    //不允许跳转
    decisionHandler(WKNavigationResponsePolicyCancel);
} 

//进程被终止时调用
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView {

}
  • WKUIDelegate :主要处理JS脚本,确认框,警告框等
/**
     *  web界面中有弹出警告框时调用
     *
     *  @param webView           实现该代理的webview
     *  @param message           警告框中的内容
     *  @param completionHandler 警告框消失调用
     */
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"HTML的弹出框" message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:([UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler();
    }])];
    [self presentViewController:alertController animated:YES completion:nil];
}
    // 确认框
    //JavaScript调用confirm方法后回调的方法 confirm是js中的确定框,需要在block中把用户选择的情况传递进去
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:([UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(NO);
    }])];
    [alertController addAction:([UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(YES);
    }])];
    [self presentViewController:alertController animated:YES completion:nil];
}
    // 输入框
    //JavaScript调用prompt方法后回调的方法 prompt是js中的输入框 需要在block中把用户输入的信息传入
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable))completionHandler{
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:prompt message:@"" preferredStyle:UIAlertControllerStyleAlert];
    [alertController addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
        textField.text = defaultText;
    }];
    [alertController addAction:([UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(alertController.textFields[0].text?:@"");
    }])];
    [self presentViewController:alertController animated:YES completion:nil];
}
    // 页面是弹出窗口 _blank 处理
- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {
    if (!navigationAction.targetFrame.isMainFrame) {
        [webView loadRequest:navigationAction.request];
    }
    return nil;
}

  • 网页内容加载进度条和title的实现,使用KVO
//添加监测网页加载进度的观察者
    [self.webView addObserver:self
                   forKeyPath:@"estimatedProgress"
                      options:0
                      context:nil];
   //添加监测网页标题title的观察者
    [self.webView addObserver:self
                   forKeyPath:@"title"
                      options:NSKeyValueObservingOptionNew
                      context:nil];

   //kvo 监听进度 必须实现此方法
-(void)observeValueForKeyPath:(NSString *)keyPath
                     ofObject:(id)object
                       change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                      context:(void *)context{
    if ([keyPath isEqualToString:NSStringFromSelector(@selector(estimatedProgress))]
        && object == self.webView) {
        
       NSLog(@"网页加载进度 = %f",_webView.estimatedProgress);
       self.progressView.progress = self.webView.estimatedProgress;
       
       if (self.webView.estimatedProgress >= 1.0f) {
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                self.progressView.progress = 0;
            });
       } 
    }else if([keyPath isEqualToString:@"title"]
             && object == self.webView){
        self.navigationItem.title = self.webView.title;
    }else{
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}

 //移除观察者,不然会引起崩溃
- (void)dealloc {
   
[self.webView removeObserver:self forKeyPath:@"estimatedProgress"];
[self.webView removeObserver:self forKeyPath:@"title"];

}
                  

WKWebView的JS和OC的交互

首先要遵守 WKScriptMessageHandler协议, WKNavigationDelegate,WKUIDelegate 代理

  • WKScriptMessageHandler协议 专门用来处理监听JavaScript方法从而调用原生OC方法
  • WKNavigationDelegate 主要处理一些跳转、加载处理操作
  • WKUIDelegate 回拦截alert、confirm、prompt三种js弹框
JS方法 WKUIDelegate方法
alert(message) -webView: runJavaScriptAlertPanelWithMessage: initiatedByFrame:completionHandler:
confirm(message) -webView:runJavaScriptConfirmPanelWithMessage:initiatedByFrame:completionHandler:
prompt(prompt, defaultText) webView:runJavaScriptTextInputPanelWithPrompt:defaultText:initiatedByFrame:completionHandler:

注意:WKUIDelegate中的三个方法都有completionHandlerblock参数,在iOS实现对应的功能后必须调用此block完成回调,否则会崩溃

其次初始化WKWebView设置这两个WKUIDelegate、WKNavigationDelegate


//webview添加配置
[self configWKWebView];

 //加载本地的html
NSString *path = [[[NSBundle mainBundle] bundlePath]  stringByAppendingPathComponent:@"test.html"];

NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL fileURLWithPath:path]];
[self.webView loadRequest:request];

给WKWebView添加配置

//给wkwebview添加配置
- (void)configWKWebView {
    
    WKWebViewConfiguration *config = [WKWebViewConfiguration new];
    //JS调用OC方法
    // WKScriptMessageHandler:这个协议类专门用来处理监听JavaScript方法从而调用原生OC方法
    [config.userContentController addScriptMessageHandler:self name:@"getUserIdFromOC"];
    
    
    self.webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height) 
    configuration:config];
     self.webView.UIDelegate = self;
    self.webView.navigationDelegate = self;
    [self.view addSubview:self.webView];
    
}

OC调用JS

当webview加载完成时候,再使用OC调用JS

- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
    
    //OC调用JS
    [self getParamsFromOC];
}

- (void)getParamsFromOC {
    
    //js的方法名和参数,把OC的userId传到js里
    NSString *js = @"getParamsFromOC('张三','18岁')";
    
    [self.webView evaluateJavaScript:js completionHandler:^(id _Nullable response, NSError * _Nullable error) {
        
        //当js里面的方法有返回值时候,response就会有值,没有为null
        NSLog(@"response: %@ error: %@", response, error);
    }];
}

我把JS里面主要的代码写出来, 在<script></script>

function getParamsFromOC(responseData1,responseData2) {
                    
    alert(responseData1+responseData2);
    //有返回值
    return {'userId':'123456'};
}

getParamsFromOC这个方法名就是OC调用JS的方法名字,传了两个参数值张三、18岁
return {'userId':'123456'};有返回值的情况下,在evaluateJavaScript:js completionHandler: 打印response数据:

response: {
    userId = 123456;
}

如果没有return,默认是return null;,所以打印为null

JS调用OC

下面这个方法就是调用JS的getUserIdFromOC方法

//相当于注册js的方法
[config.userContentController 
addScriptMessageHandler:self name:@"getUserIdFromOC"];

在H5页面的body里添加一个button,点击事件

<input type="button" value="WKWebView调用OC方法" 
onclick="getUserIdFromOC({'userId':'123456'});"/>

在Html中 在<script></script>中添加方法


function getUserIdFromOC(responseData) {
                        
    //WKWebView调用oc方法,规定的写法
    window.webkit.messageHandlers.
    getUserIdFromOC.postMessage(responseData);
}
 

PS:window.webkit.messageHandlers.
方法名.postMessage()这个是WKWebView的统一写法。这样的话,JS就调用了OC的方法

JS调用了OC的方法,会触发下面WKScriptMessageHandler协议方法

//WKScriptMessageHandler 协议
- (void)userContentController:(nonnull WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message {
    
    /*
     message.body: 触发js方法传的值
     message.name: 触发js的方法名
     */
    NSLog(@"传的值L: --%@ \n 方法名: --- %@",message.body, message.name);
    //如果是JS的这个getUserIdFromOC方法
    if ([message.name isEqualToString:@"getUserIdFromOC"]) {
        
        NSLog(@"调用%@成功,传的值:%@",message.name, message.body);
        //对这个方法进行处理操作
    }
}

点击heml的按钮,触发OC的方法,打印结果如下:

调用getUserIdFromOC成功,传的值:{
    userId = 123456;
}

所以在WKWebView中OC与JS交互的整个流程已经完毕。

结尾

WKWebView是iOS8之后才出现的,使用了
所以要适配iOS8之前的,针对系统判断来进行用UIWebView还是WKWebView进行OC与JS交互。现在苹果已经XCode已经不针对iOS8一下的进行适配,所以大家尽量使用WKWebView来做处理。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容