前言
最近項(xiàng)目開發(fā)中用到了OC與JS交互方面的知識(shí)幽钢,以前也用過UIWebView JS與OC交互方面的班眯,使用的蘋果在iOS7開放的javascriptCore框架,使用起來挺方便快捷,javascriptCore源碼是開放的,有興趣的可以去了解一下梦碗。
自從iOS8,蘋果就UIWebView性能不好,推出了WKWebView蓖救,以及github上評(píng)分很高的WebViewJavascriptBridge里面最新版本也最WKWebView做了兼容叉弦。
實(shí)現(xiàn)的方式
所以我總結(jié)的方式,分UIWebview藻糖、WKWebView、以及通用版本的第三方WebViewJavascriptBridge库车,進(jìn)行實(shí)現(xiàn)巨柒。
- UIWebView中JS與OC交互
- WKWebView中JS與OC交互(只能iOS8及之后的版本)
- WebViewJavascriptBridge對(duì)UIWebView與WKWebView做了統(tǒng)一處理。
WKWebView出現(xiàn)背景和優(yōu)點(diǎn)
UIWebView自iOS2就有柠衍,WKWebView從iOS8才有洋满,毫無疑問WKWebView將逐步取代笨重的UIWebView。
通過簡單的測試即可發(fā)現(xiàn)UIWebView占用過多內(nèi)存珍坊,且內(nèi)存峰值更是夸張牺勾。WKWebView網(wǎng)頁加載速度也有提升,但是并不像內(nèi)存那樣提升那么多
下面列舉一些其它的優(yōu)勢(shì):
更多的支持HTML5的特性
官方宣稱的高達(dá)60fps的滾動(dòng)刷新率以及內(nèi)置手勢(shì)
Safari相同的JavaScript引擎
將UIWebViewDelegate與UIWebView拆分成了14類與3個(gè)協(xié)議(官方文檔說明)
另外用的比較多的阵漏,增加加載進(jìn)度屬性:estimatedProgress
了解WKWebView要涉及一些類
首頁使用WKWebView要引進(jìn):
#import <WebKit/WebKit.h>
- WKWebView使用
//初始化
self.webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT) configuration:config];
// UI代理
self.webView.UIDelegate = self;
// 導(dǎo)航代理 self.webView.navigationDelegate = self;
// 是否允許手勢(shì)左滑返回上一級(jí), 類似導(dǎo)航控制的左滑返回 self.webView.allowsBackForwardNavigationGestures = YES;
//可返回的頁面列表, 存儲(chǔ)已打開過的網(wǎng)頁
WKBackForwardList * backForwardList = [self.webView backForwardList];
//頁面后退
[self.webView goBack];
//頁面前進(jìn)
[self.webView goForward];
//刷新當(dāng)前頁面
[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配置信息
//創(chuàng)建網(wǎng)頁配置對(duì)象
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
// 創(chuàng)建設(shè)置對(duì)象
WKPreferences *preference = [[WKPreferences alloc]init];
//最小字體大小 當(dāng)將javaScriptEnabled屬性設(shè)置為NO時(shí)驻民,可以看到明顯的效果
preference.minimumFontSize = 0;
//設(shè)置是否支持javaScript 默認(rèn)是支持的
preference.javaScriptEnabled = YES;
// 在iOS上默認(rèn)為NO,表示是否允許不經(jīng)過用戶交互由javaScript自動(dòng)打開窗口
preference.javaScriptCanOpenWindowsAutomatically = YES;
config.preferences = preference;
// 是使用h5的視頻播放器在線播放, 還是使用原生播放器全屏播放
config.allowsInlineMediaPlayback = YES;
//設(shè)置視頻是否需要用戶手動(dòng)播放 設(shè)置為NO則會(huì)允許自動(dòng)播放
config.requiresUserActionForMediaPlayback = YES;
//設(shè)置是否允許畫中畫技術(shù) 在特定設(shè)備上有效
config.allowsPictureInPictureMediaPlayback = YES;
//設(shè)置請(qǐng)求的User-Agent信息中應(yīng)用程序名稱 iOS9后可用
config.applicationNameForUserAgent = @"ChinaDailyForiPad";
//自定義的WKScriptMessageHandler 是為了解決內(nèi)存不釋放的問題
WeakWebViewScriptMessageDelegate *weakScriptMessageDelegate = [[WeakWebViewScriptMessageDelegate alloc] initWithDelegate:self];
//這個(gè)類主要用來做native與JavaScript的交互管理
WKUserContentController * wkUController = [[WKUserContentController alloc] init];
//注冊(cè)一個(gè)name為jsToOcNoPrams的js方法
[wkUController addScriptMessageHandler:weakScriptMessageDelegate name:@"js調(diào)用OC的方法"];
[wkUController addScriptMessageHandler:weakScriptMessageDelegate name:@"js調(diào)用OC的方法"];
config.userContentController = wkUController;
- WKUserScript:用于進(jìn)行JavaScript注入
//以下代碼適配文本大小履怯,由UIWebView換為WKWebView后回还,會(huì)發(fā)現(xiàn)字體小了很多,這應(yīng)該是WKWebView與html的兼容問題叹洲,解決辦法是修改原網(wǎng)頁柠硕,要么我們手動(dòng)注入JS
NSString *jSString = @"var meta = document.createElement('meta');
meta.setAttribute('name', 'viewport');
meta.setAttribute('content',
'width=device-width');
document.getElementsByTagName('head')
[0].appendChild(meta);";
//用于進(jìn)行JavaScript注入
WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jSString
injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
[config.userContentController addUserScript:wkUScript];
- WKUserContentController:這個(gè)類主要用來做native與JavaScript的交互管理
//這個(gè)類主要用來做native與JavaScript的交互管理
WKUserContentController * wkUController = [[WKUserContentController alloc] init];
//注冊(cè)一個(gè)name為jsToOcNoPrams的js方法,設(shè)置處理接收J(rèn)S方法的代理
[wkUController addScriptMessageHandler:self name:@"jsCallOCNoPrams"];
[wkUController addScriptMessageHandler:self name:@"jsCallOCNoPrams"];
config.userContentController = wkUController;
//用完記得移除
//移除注冊(cè)的js方法,避免內(nèi)存泄露
[[self.webView configuration].userContentController
removeScriptMessageHandlerForName:@"jsToOcNoPrams"];
[[self.webView configuration].userContentController
removeScriptMessageHandlerForName:@"jsToOcWithPrams"];
- WKScriptMessageHandler:這個(gè)協(xié)議類專門用來處理監(jiān)聽JavaScript方法從而調(diào)用原生OC方法运提,和WKUserContentController搭配使用蝗柔。
注意:遵守WKScriptMessageHandler協(xié)議闻葵,代理是由WKUserContentControl設(shè)置
//通過接收J(rèn)S傳出消息的name進(jìn)行捕捉的回調(diào)方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
/*
message.body: 觸發(fā)js方法傳的值
message.name: 觸發(fā)js的方法名
*/
NSLog(@"傳的值L: --%@ \n 方法名: --- %@",message.body, message.name);
}
- WKNavigationDelegate :主要處理一些跳轉(zhuǎn)、加載處理操作
// 頁面開始加載時(shí)調(diào)用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
}
// 頁面加載失敗時(shí)調(diào)用
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error {
}
// 當(dāng)內(nèi)容開始返回時(shí)調(diào)用
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
}
// 頁面加載完成之后調(diào)用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
}
//提交發(fā)生錯(cuò)誤時(shí)調(diào)用
- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error {
}
// 接收到服務(wù)器跳轉(zhuǎn)請(qǐng)求即服務(wù)重定向時(shí)之后調(diào)用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation {
}
// 根據(jù)WebView對(duì)于即將跳轉(zhuǎn)的HTTP請(qǐng)求頭信息和相關(guān)信息來決定是否跳轉(zhuǎn)
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
}
// 根據(jù)客戶端受到的服務(wù)器響應(yīng)頭以及response相關(guān)信息來決定是否可以跳轉(zhuǎn)
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
NSString * urlStr = navigationResponse.response.URL.absoluteString;
NSLog(@"當(dāng)前跳轉(zhuǎn)地址:%@",urlStr);
//允許跳轉(zhuǎn)
decisionHandler(WKNavigationResponsePolicyAllow);
//不允許跳轉(zhuǎn)
decisionHandler(WKNavigationResponsePolicyCancel);
}
//進(jìn)程被終止時(shí)調(diào)用
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView {
}
- WKUIDelegate :主要處理JS腳本癣丧,確認(rèn)框槽畔,警告框等
/**
* web界面中有彈出警告框時(shí)調(diào)用
*
* @param webView 實(shí)現(xiàn)該代理的webview
* @param message 警告框中的內(nèi)容
* @param completionHandler 警告框消失調(diào)用
*/
- (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];
}
// 確認(rèn)框
//JavaScript調(diào)用confirm方法后回調(diào)的方法 confirm是js中的確定框,需要在block中把用戶選擇的情況傳遞進(jìn)去
- (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調(diào)用prompt方法后回調(diào)的方法 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;
}
- 網(wǎng)頁內(nèi)容加載進(jìn)度條和title的實(shí)現(xiàn)坎缭,使用KVO
//添加監(jiān)測網(wǎng)頁加載進(jìn)度的觀察者
[self.webView addObserver:self
forKeyPath:@"estimatedProgress"
options:0
context:nil];
//添加監(jiān)測網(wǎng)頁標(biāo)題title的觀察者
[self.webView addObserver:self
forKeyPath:@"title"
options:NSKeyValueObservingOptionNew
context:nil];
//kvo 監(jiān)聽進(jìn)度 必須實(shí)現(xiàn)此方法
-(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(@"網(wǎng)頁加載進(jìn)度 = %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];
}
}
//移除觀察者,不然會(huì)引起崩潰
- (void)dealloc {
[self.webView removeObserver:self forKeyPath:@"estimatedProgress"];
[self.webView removeObserver:self forKeyPath:@"title"];
}
WKWebView的JS和OC的交互
首先要遵守 WKScriptMessageHandler協(xié)議, WKNavigationDelegate,WKUIDelegate 代理
- WKScriptMessageHandler協(xié)議 專門用來處理監(jiān)聽JavaScript方法從而調(diào)用原生OC方法
- WKNavigationDelegate 主要處理一些跳轉(zhuǎn)竟痰、加載處理操作
- WKUIDelegate 回?cái)r截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中的三個(gè)方法都有completionHandlerblock參數(shù)坏快,在iOS實(shí)現(xiàn)對(duì)應(yīng)的功能后必須調(diào)用此block完成回調(diào),否則會(huì)崩潰
其次初始化WKWebView設(shè)置這兩個(gè)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調(diào)用OC方法
// WKScriptMessageHandler:這個(gè)協(xié)議類專門用來處理監(jiān)聽JavaScript方法從而調(diào)用原生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調(diào)用JS
當(dāng)webview加載完成時(shí)候莽鸿,再使用OC調(diào)用JS
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
//OC調(diào)用JS
[self getParamsFromOC];
}
- (void)getParamsFromOC {
//js的方法名和參數(shù),把OC的userId傳到j(luò)s里
NSString *js = @"getParamsFromOC('張三','18歲')";
[self.webView evaluateJavaScript:js completionHandler:^(id _Nullable response, NSError * _Nullable error) {
//當(dāng)js里面的方法有返回值時(shí)候,response就會(huì)有值拾给,沒有為null
NSLog(@"response: %@ error: %@", response, error);
}];
}
我把JS里面主要的代碼寫出來, 在<script></script>
function getParamsFromOC(responseData1,responseData2) {
alert(responseData1+responseData2);
//有返回值
return {'userId':'123456'};
}
getParamsFromOC這個(gè)方法名就是OC調(diào)用JS的方法名字祥得,傳了兩個(gè)參數(shù)值張三、18歲
return {'userId':'123456'};有返回值的情況下蒋得,在evaluateJavaScript:js completionHandler: 打印response數(shù)據(jù):
response: {
userId = 123456;
}
如果沒有return级及,默認(rèn)是return null;额衙,所以打印為null
JS調(diào)用OC
下面這個(gè)方法就是調(diào)用JS的getUserIdFromOC方法
//相當(dāng)于注冊(cè)js的方法
[config.userContentController
addScriptMessageHandler:self name:@"getUserIdFromOC"];
在H5頁面的body里添加一個(gè)button饮焦,點(diǎn)擊事件
<input type="button" value="WKWebView調(diào)用OC方法"
onclick="getUserIdFromOC({'userId':'123456'});"/>
在Html中 在<script></script>中添加方法
function getUserIdFromOC(responseData) {
//WKWebView調(diào)用oc方法,規(guī)定的寫法
window.webkit.messageHandlers.
getUserIdFromOC.postMessage(responseData);
}
PS:window.webkit.messageHandlers.
方法名.postMessage()這個(gè)是WKWebView的統(tǒng)一寫法窍侧。這樣的話县踢,JS就調(diào)用了OC的方法
JS調(diào)用了OC的方法,會(huì)觸發(fā)下面WKScriptMessageHandler協(xié)議方法
//WKScriptMessageHandler 協(xié)議
- (void)userContentController:(nonnull WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message {
/*
message.body: 觸發(fā)js方法傳的值
message.name: 觸發(fā)js的方法名
*/
NSLog(@"傳的值L: --%@ \n 方法名: --- %@",message.body, message.name);
//如果是JS的這個(gè)getUserIdFromOC方法
if ([message.name isEqualToString:@"getUserIdFromOC"]) {
NSLog(@"調(diào)用%@成功伟件,傳的值:%@",message.name, message.body);
//對(duì)這個(gè)方法進(jìn)行處理操作
}
}
點(diǎn)擊heml的按鈕硼啤,觸發(fā)OC的方法,打印結(jié)果如下:
調(diào)用getUserIdFromOC成功斧账,傳的值:{
userId = 123456;
}
所以在WKWebView中OC與JS交互的整個(gè)流程已經(jīng)完畢谴返。
結(jié)尾
WKWebView是iOS8之后才出現(xiàn)的,使用了
所以要適配iOS8之前的咧织,針對(duì)系統(tǒng)判斷來進(jìn)行用UIWebView還是WKWebView進(jìn)行OC與JS交互】髁現(xiàn)在蘋果已經(jīng)XCode已經(jīng)不針對(duì)iOS8一下的進(jìn)行適配,所以大家盡量使用WKWebView來做處理拯爽。