1. 概述
從iOS8開始,就引入了新的瀏覽器控件WKWebView览妖,用于取代UIWebView,但是由于UIWebView的簡(jiǎn)單易用揽祥,還是使用率很高讽膏,目前蘋果已經(jīng)在迭代時(shí),會(huì)發(fā)警告??提醒更換控件盔然,新應(yīng)用必須使用WKWebView桅打,到了告別UIWebView的時(shí)候了....
那么WKWebView究竟好在哪里呢是嗜?
- 內(nèi)存開銷更小
- 內(nèi)置手勢(shì)
- 支持更多H5特性
- 有Safari相同的JavaScript引擎
- 提供更多屬性愈案,比如加載進(jìn)度、標(biāo)題鹅搪、準(zhǔn)確的得到頁(yè)面數(shù)等等
- 提供了更精細(xì)的加載流程回調(diào)(當(dāng)然相比UIWebView看起來也更麻煩一些站绪,畢竟方法多了)
1.1 UIWebView和WKWebView的流程對(duì)比
WKWebView的流程粒度更加細(xì)致,不但在請(qǐng)求的時(shí)候會(huì)詢問WKWebView是否請(qǐng)求數(shù)據(jù)丽柿,還會(huì)在返回?cái)?shù)據(jù)之后詢問WKWebView是否加載數(shù)據(jù)
我曾經(jīng)有個(gè)需求恢准,點(diǎn)擊鏈接的時(shí)候,如果是圖片那就下載而不是跳轉(zhuǎn)甫题,用UIWebView就不好做馁筐,因?yàn)槟悴恢梨溄訉?duì)應(yīng)的到底是什么文件(有重定向),如果用WKWebView坠非,我就可以在數(shù)據(jù)返回的時(shí)候判斷MIMEType做出不同的跳轉(zhuǎn)策略
2. WKWebView的基本使用
2.1 引入WKWebView
- 頭文件引入
#import <WebKit/WebKit.h>
-
在targets中添加WebKit.framework庫(kù)
WebKit.framework
2.2 WKWebView初始化
可以在初始化的時(shí)候,加入一些配置選項(xiàng)
- (WKWebView *)webView
{
if (nil == _webView) {
// 可以做一些初始化配置定制
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
configuration.selectionGranularity = WKSelectionGranularityDynamic;
configuration.allowsInlineMediaPlayback = YES;
WKPreferences *preferences = [WKPreferences new];
//是否支持JavaScript
preferences.javaScriptEnabled = YES;
//不通過用戶交互,是否可以打開窗口
preferences.javaScriptCanOpenWindowsAutomatically = YES;
configuration.preferences = preferences;
// 初始化WKWebView
_webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds configuration:configuration];
// 有兩種代理盟迟,UIDelegate負(fù)責(zé)界面彈窗秋泳,navigationDelegate負(fù)責(zé)加載、跳轉(zhuǎn)等
_webView.UIDelegate = self;
_webView.navigationDelegate = self;
}
return _webView;
}
2.3 WKNavigationDelegate協(xié)議方法
#pragma mark - WKNavigationDelegate
/* 頁(yè)面開始加載 */
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation{
}
/* 開始返回內(nèi)容 */
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation{
}
/* 頁(yè)面加載完成 */
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation{
}
/* 頁(yè)面加載失敗 */
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation{
}
/* 在發(fā)送請(qǐng)求之前攒菠,決定是否跳轉(zhuǎn) */
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
//允許跳轉(zhuǎn)
decisionHandler(WKNavigationActionPolicyAllow);
//不允許跳轉(zhuǎn)
//decisionHandler(WKNavigationActionPolicyCancel);
}
/* 在收到響應(yīng)后迫皱,決定是否跳轉(zhuǎn) */
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{
NSLog(@"%@",navigationResponse.response.URL.absoluteString);
//允許跳轉(zhuǎn)
decisionHandler(WKNavigationResponsePolicyAllow);
//不允許跳轉(zhuǎn)
//decisionHandler(WKNavigationResponsePolicyCancel);
}
2.4 UIDelegate協(xié)議的主要方法及應(yīng)用
特別需要注意這個(gè)協(xié)議,與UIWebView不同辖众,在WKWebView中卓起,如果H5頁(yè)面調(diào)用了window對(duì)象的alert,confirm,prompt方法,默認(rèn)不會(huì)有任何反應(yīng)凹炸,它內(nèi)部會(huì)回調(diào)給你既绩,必須由原生這邊實(shí)現(xiàn)相關(guān)的彈窗
感覺這東西設(shè)計(jì)的很雞肋,特別是初學(xué)者很喜歡alert一下看效果还惠,結(jié)果一直點(diǎn)沒反應(yīng)饲握,真是個(gè)大坑
#pragma mark - WKNavigationDelegate
#pragma mark - WKUIDelegate
/// 處理alert彈窗事件
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{
[self alert:@"溫馨提示" message:message?:@"" buttonTitles:@[@"確認(rèn)"] handler:^(int index, NSString *title) {
completionHandler();
}];
}
/// 處理Confirm彈窗事件
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{
[self alert:@"溫馨提示" message:message?:@"" buttonTitles:@[@"取消", @"確認(rèn)"] handler:^(int index, NSString *title) {
completionHandler(index != 0);
}];
}
/// 處理TextInput彈窗事件
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable))completionHandler {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"溫馨提示" message:prompt preferredStyle:UIAlertControllerStyleAlert];
[alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
textField.text = defaultText;
}];
[alert addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
completionHandler(nil);
}]];
[alert addAction:[UIAlertAction actionWithTitle:@"確定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
NSString *text = [alert.textFields firstObject].text;
NSLog(@"字符串:%@", text);
completionHandler(text);
}]];
[self presentViewController:alert animated:YES completion:nil];
}
#pragma mark - 彈窗
- (void)alert:(NSString *)title message:(NSString *)message {
[self alert:title message:message buttonTitles:@[@"確定"] handler:nil];
}
- (void)alert:(NSString *)title message:(NSString *)message buttonTitles:(NSArray *)buttonTitles handler:(void(^)(int, NSString *))handler {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
for (int i = 0; i < buttonTitles.count; i++) {
[alert addAction:[UIAlertAction actionWithTitle:buttonTitles[i] style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
if (handler) {
handler(i, action.title);
}
}]];
}
[self presentViewController:alert animated:YES completion:nil];
}
3. WKWebView更多實(shí)戰(zhàn)細(xì)節(jié)
3.1 動(dòng)態(tài)更新標(biāo)題
導(dǎo)航欄標(biāo)題經(jīng)常要根據(jù)當(dāng)前H5頁(yè)面標(biāo)題更換,以前都是在頁(yè)面加載完成后蚕键,使用
window.document.title
來獲取救欧,現(xiàn)在WKWebView提供了相關(guān)字段,我們只需要監(jiān)聽這個(gè)字段即可
[self.webView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:NULL];
#pragma mark - 屬性監(jiān)聽
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"title"]) {
NSString *title = (NSString *)change[NSKeyValueChangeNewKey];
self.title = title;
}
}
3.2 動(dòng)態(tài)加載進(jìn)度條
以前UIWKWebView無法獲取加載進(jìn)度锣光,只能知曉開始加載和結(jié)束加載笆怠,因此以前的做法是做一個(gè)假的進(jìn)度條,等到結(jié)束的時(shí)候再突然設(shè)置成100%
WKWebView提供了estimatedProgress來監(jiān)聽加載進(jìn)度誊爹,提供了loading來獲取加載狀態(tài)蹬刷,我們可以拖個(gè)UIProgressView來顯示進(jìn)度(也很多人用layer來做,還可以做漸變的效果频丘,視覺上更優(yōu))
@property (weak, nonatomic) IBOutlet UIProgressView *progressView;
[self.webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:NULL];
[self.webView addObserver:self forKeyPath:@"loading" options:NSKeyValueObservingOptionNew context:NULL];
#pragma mark - 屬性監(jiān)聽
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"estimatedProgress"]) {
CGFloat estimatedProgress = [change[NSKeyValueChangeNewKey] floatValue];
NSLog(@"頁(yè)面加載進(jìn)度:%f", estimatedProgress);
[self.progressView setProgress:estimatedProgress];
}else if ([keyPath isEqualToString:@"loading"]) {
BOOL loading = [change[NSKeyValueChangeNewKey] boolValue];
NSLog(@"%@", loading ? @"開始加載" : @"停止加載");
self.progressView.hidden = !loading;
}
}
3.3 獲取已打開頁(yè)面數(shù)量
UIWebView提供了pagecount办成,但是沒有卵用,不準(zhǔn)確搂漠;WKWebView中有backForwardList記錄了可回退的頁(yè)面信息迂卢,
已打開頁(yè)面數(shù)量 = backForwardList數(shù)量 + 當(dāng)前1頁(yè)
int pageCount = self.webView.backForwardList.count + 1;
4. JS交互
4.1 原生調(diào)H5
簡(jiǎn)單易用,第一參數(shù)傳執(zhí)行的js方法桐汤,第二個(gè)block中回調(diào)執(zhí)行后的結(jié)果而克,如果沒有返回值,可以忽略這個(gè)block
[self.webView evaluateJavaScript:@"prompt('請(qǐng)輸入您的名字:', '哈利波特')" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
if (error) {
NSLog(@"error: %@", error);
}else {
NSLog(@"obj: %@", result);
}
}];
4.2 H5調(diào)原生
- 在UIWebView中怔毛,H5觸發(fā)原生的函數(shù)员萍,我們普遍做法約定好需要觸發(fā)事件的鏈接規(guī)則,如果是普通超鏈接就放行拣度,如果是特殊鏈接碎绎,就攔截下來蜂莉,然后根據(jù)約定好的規(guī)則,拼湊出要調(diào)用的方法名稱混卵、參數(shù)等等信息
- 在WKWebView中映穗,有一套解決js調(diào)用原生方法的規(guī)則
步驟:
-
window.webkit.messageHandlers.<#對(duì)象名#>.postMessage(<#參數(shù)#>)
,這個(gè)對(duì)象名稱只是個(gè)別名(不是非要對(duì)應(yīng)我們哪個(gè)對(duì)象名稱)幕随,跟前端協(xié)商好即可蚁滋,比如我這里起名“target”
<script>
$("#shoot").click(function () {
// 這里按照約定好的規(guī)則,觸發(fā)的時(shí)候按照特定對(duì)象發(fā)送消息赘淮,傳達(dá)到原生中
// 實(shí)際開發(fā)中辕录,還要考慮多端交互的兼容性問題(iOS、Android梢卸、wechat)
window.webkit.messageHandlers.target.postMessage({action: '開槍射擊'});
});
$("#refull").click(function () {
window.webkit.messageHandlers.target.postMessage({action: '上子彈'});
});
</script>
- 在iOS端走诞,添加js腳本的響應(yīng)對(duì)象
注冊(cè)告訴WKWebView都有哪些對(duì)象要來響應(yīng)js事件,分別叫什么名字
// H5調(diào)用原生格式:window.webkit.messageHandlers.{name}.postMessage(參數(shù));
// window.webkit.messageHandlers.target.postMessage({action: '開槍射擊'});
// 在原生中注冊(cè)好能響應(yīng)js方法的類和注冊(cè)別名蛤高,然后js按照以上方式調(diào)用蚣旱,可以進(jìn)入OC的didReceiveScriptMessage代理方法
[userContentController addScriptMessageHandler:self name:target];
- 響應(yīng)對(duì)象實(shí)現(xiàn)相關(guān)協(xié)議
WKWebView會(huì)把觸發(fā)回調(diào)給我們的協(xié)議方法,響應(yīng)對(duì)象實(shí)現(xiàn)它即可
#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
NSString *name = [NSString stringWithFormat:@"執(zhí)行對(duì)象名稱:%@", message.name];
NSString *params = [NSString stringWithFormat:@"附帶參數(shù):%@", [message.body description]];
}
4.2.1 H5調(diào)原生的內(nèi)存泄露問題
問題:當(dāng)執(zhí)行
addScriptMessageHandler
方法時(shí)戴陡,如果傳入的是當(dāng)前控制器塞绿,控制器會(huì)被WKWebView強(qiáng)引用(就算你傳入weak都沒用,內(nèi)部還是轉(zhuǎn)成強(qiáng)引用)恤批,而當(dāng)前控制器強(qiáng)引用著WKWebView异吻,就成了循環(huán)引用
解決方式
方式一
在合適的時(shí)機(jī)添加和移除
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// 注冊(cè)響應(yīng)H5調(diào)原生埋點(diǎn)
WKUserContentController *userContentController = self.webView.configuration.userContentController;
// H5調(diào)用原生格式:window.webkit.messageHandlers.{name}.postMessage(參數(shù));
// window.webkit.messageHandlers.target.postMessage({action: '開槍射擊'});
// 在原生中注冊(cè)好能響應(yīng)js方法的類和注冊(cè)別名,然后js按照以上方式調(diào)用喜庞,可以進(jìn)入OC的didReceiveScriptMessage代理方法
[userContentController addScriptMessageHandler:self name:KCNAME];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// 注冊(cè)過的對(duì)象诀浪,移除,否則有內(nèi)存泄露的問題
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:KCNAME];
}
方式二
其實(shí)蘋果這么設(shè)計(jì)延都,應(yīng)該是希望我們傳入一個(gè)單獨(dú)實(shí)現(xiàn)了WKScriptMessageHandler的對(duì)象雷猪,用來響應(yīng)相關(guān)js交互操作,而不是傳入當(dāng)前控制器
參考文章:https://www.cnblogs.com/guohai-stronger/p/10234571.html