前言
最近项目开发中用到了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来做处理。