Xcode8發佈之後,編譯器開始不支持IOS7,因此不少應用在適配IOS10以後都不在適配IOS7了,其中包括了不少大公司,網易新聞,滴滴出行等。所以,咱們公司的應用也打算淘汰IOS7。javascript
支持到IOS8,第一個要改的天然是用WKWebView
替換原來的UIWebView
。WKWebView有不少明顯優點:html
更多的支持HTML5的特性前端
官方宣稱的高達60fps的滾動刷新率以及內置手勢java
將UIWebViewDelegate與UIWebView拆分紅了14類與3個協議,之前不少不方便實現的功能得以實現。文檔git
Safari相同的JavaScript引擎github
佔用更少的內存web
UIWebViewjson
WKWebView緩存
所以,使用WkWebview
替換UIWebView
仍是頗有必要的。服務器
WKWebView有兩個delegate,WKUIDelegate 和 WKNavigationDelegate。WKNavigationDelegate主要處理一些跳轉、加載處理操做,WKUIDelegate主要處理JS腳本,確認框,警告框等。所以WKNavigationDelegate更加經常使用。
比較經常使用的方法:
#pragma mark - lifeCircle - (void)viewDidLoad { [super viewDidLoad]; webView = [[WKWebView alloc]init]; [self.view addSubview:webView]; [webView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.view); make.right.equalTo(self.view); make.top.equalTo(self.view); make.bottom.equalTo(self.view); }]; webView.UIDelegate = self; webView.navigationDelegate = self; [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]]]; } #pragma mark - WKNavigationDelegate // 頁面開始加載時調用 - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation{ } // 當內容開始返回時調用 - (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation{ } // 頁面加載完成以後調用 - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation{ } // 頁面加載失敗時調用 - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation{ } // 接收到服務器跳轉請求以後調用 - (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation{ } // 在收到響應後,決定是否跳轉 - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{ NSLog(@"%@",navigationResponse.response.URL.absoluteString); //容許跳轉 decisionHandler(WKNavigationResponsePolicyAllow); //不容許跳轉 //decisionHandler(WKNavigationResponsePolicyCancel); } // 在發送請求以前,決定是否跳轉 - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{ NSLog(@"%@",navigationAction.request.URL.absoluteString); //容許跳轉 decisionHandler(WKNavigationActionPolicyAllow); //不容許跳轉 //decisionHandler(WKNavigationActionPolicyCancel); } #pragma mark - WKUIDelegate // 建立一個新的WebView - (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures{ return [[WKWebView alloc]init]; } // 輸入框 - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * __nullable result))completionHandler{ completionHandler(@"http"); } // 確認框 - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler{ completionHandler(YES); } // 警告框 - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{ NSLog(@"%@",message); completionHandler(); }
WKWebview提供了API實現js交互 不須要藉助JavaScriptCore或者webJavaScriptBridge。使用WKUserContentController實現js native交互。簡單的說就是先註冊約定好的方法,而後再調用。
oc代碼(有誤,內存不釋放):
@interface ViewController ()<WKUIDelegate,WKNavigationDelegate,WKScriptMessageHandler>{ WKWebView * webView; WKUserContentController* userContentController; } @end @implementation ViewController #pragma mark - lifeCircle - (void)viewDidLoad { [super viewDidLoad]; //配置環境 WKWebViewConfiguration * configuration = [[WKWebViewConfiguration alloc]init]; userContentController =[[WKUserContentController alloc]init]; configuration.userContentController = userContentController; webView = [[WKWebView alloc]initWithFrame:CGRectMake(0, 0, 100, 100) configuration:configuration]; //註冊方法 [userContentController addScriptMessageHandler:self name:@"sayhello"];//註冊一個name爲sayhello的js方法 [self.view addSubview:webView]; [webView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.view); make.right.equalTo(self.view); make.top.equalTo(self.view); make.bottom.equalTo(self.view); }]; webView.UIDelegate = self; webView.navigationDelegate = self; [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.test.com"]]]; } - (void)dealloc{ //這裏須要注意,前面增長過的方法必定要remove掉。 [userContentController removeScriptMessageHandlerForName:@"sayhello"]; } #pragma mark - WKScriptMessageHandler - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{ NSLog(@"name:%@\\\\n body:%@\\\\n frameInfo:%@\\\\n",message.name,message.body,message.frameInfo); } @end
上面的OC代碼若是認證測試一下就會發現dealloc並不會執行,這樣確定是不行的,會形成內存泄漏。緣由是[userContentController addScriptMessageHandler:self name:@"sayhello"];
這句代碼形成沒法釋放內存。(ps:試了下用weak指針仍是不能釋放,不知道是什麼緣由。)所以還須要進一步改進,正確的寫法是用一個新的controller來處理,新的controller再繞用delegate繞回來。
oc代碼(正確寫法):
@interface ViewController ()<WKUIDelegate,WKNavigationDelegate,WKScriptMessageHandler>{ WKWebView * webView; WKUserContentController* userContentController; } @end @implementation ViewController #pragma mark - lifeCircle - (void)viewDidLoad { [super viewDidLoad]; //配置環境 WKWebViewConfiguration * configuration = [[WKWebViewConfiguration alloc]init]; userContentController =[[WKUserContentController alloc]init]; configuration.userContentController = userContentController; webView = [[WKWebView alloc]initWithFrame:CGRectMake(0, 0, 100, 100) configuration:configuration]; //註冊方法 WKDelegateController * delegateController = [[WKDelegateController alloc]init]; delegateController.delegate = self; [userContentController addScriptMessageHandler:delegateController name:@"sayhello"]; [self.view addSubview:webView]; [webView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.view); make.right.equalTo(self.view); make.top.equalTo(self.view); make.bottom.equalTo(self.view); }]; webView.UIDelegate = self; webView.navigationDelegate = self; [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.test.com"]]]; } - (void)dealloc{ //這裏須要注意,前面增長過的方法必定要remove掉。 [userContentController removeScriptMessageHandlerForName:@"sayhello"]; } #pragma mark - WKScriptMessageHandler - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{ NSLog(@"name:%@\\\\n body:%@\\\\n frameInfo:%@\\\\n",message.name,message.body,message.frameInfo); } @end
WKDelegateController代碼:
#import <UIKit/UIKit.h> #import <WebKit/WebKit.h> @protocol WKDelegate <NSObject> - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message; @end @interface WKDelegateController : UIViewController <WKScriptMessageHandler> @property (weak , nonatomic) id<WKDelegate> delegate; @end
.m代碼:
#import "WKDelegateController.h" @interface WKDelegateController () @end @implementation WKDelegateController - (void)viewDidLoad { [super viewDidLoad]; } - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{ if ([self.delegate respondsToSelector:@selector(userContentController:didReceiveScriptMessage:)]) { [self.delegate userContentController:userContentController didReceiveScriptMessage:message]; } } @end
h5代碼:
<html> <head> <script> function say() { //前端須要用 window.webkit.messageHandlers.註冊的方法名.postMessage({body:傳輸的數據} 來給native發送消息 window.webkit.messageHandlers.sayhello.postMessage({body: 'hello world!'}); } </script> </head> <body> <h1>hello world</h1> <button onclick="say()">say hello</button> </body> </html>
打印出的log:
name:sayhello
body:{
body = "hello world!"; } frameInfo:<WKFrameInfo: 0x7f872060ce20; isMainFrame = YES; request = <NSMutableURLRequest: 0x618000010a30> { URL: http://www.test.com/ }>
addScriptMessageHandler
要和removeScriptMessageHandlerForName
配套出現,不然會形成內存泄漏。代碼以下:
- (void)webView:(WKWebView *)tmpWebView didFinishNavigation:(WKNavigation *)navigation{ //say()是JS方法名,completionHandler是異步回調block [webView evaluateJavaScript:@"say()" completionHandler:^(id _Nullable result, NSError * _Nullable error) { NSLog(@"%@",result); }]; }
h5代碼同上。
通常來講,一個好的UI總有一個大神會開發出一個好的第三方封裝框架。WebViewJavascriptBridge的做者也作了一套支持WKWebView與JS交互的第三方框架:WKWebViewJavascriptBridge。
cocoaPods: pod 'WebViewJavascriptBridge', '~> 5.0.5'
github地址:https://github.com/marcuswestin/WebViewJavascriptBridge
主要方法以下:
//初始化方法 + (instancetype)bridgeForWebView:(WKWebView*)webView; + (void)enableLogging; //註冊函數名 - (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler; //調用函數名 - (void)callHandler:(NSString*)handlerName; - (void)callHandler:(NSString*)handlerName data:(id)data; - (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback; //重置 - (void)reset; //設置WKNavigationDelegate - (void)setWebViewDelegate:(id<WKNavigationDelegate>)webViewDelegate;
基本的實現方法和上面寫的差很少,就是封裝了一下,有興趣的童鞋能夠本身pod下來使用。
iOS8
以後,蘋果推出了WebKit
這個框架,用來替換原有的UIWebView
,新的控件優勢多多,不一一敘述。因爲一直在適配iOS7
,就沒有去替換,如今仍掉了iOS7
,覺得很簡單的就替換過來了,然而在替換的過程當中,卻遇到了不少坑。還有一點就是原來寫過一篇文章 Objective-C與JavaScript交互的那些事覺得年代久遠的UIWebView
已經做古,可這篇文章如今依然有必定的閱讀量。因此在決定在續一篇此文,以引導你們轉向WKWebView
,並指出本身踩過的坑,讓你們少走彎路。
此篇文章的邏輯圖
WKWebView
只能用代碼建立,並且自身就支持了右滑返回手勢allowsBackForwardNavigationGestures
和加載進度estimatedProgress
等一些UIWebView
不具有卻很是好用的屬性。在建立的時候,指定初始化方法中要求傳入一個WKWebViewConfiguration
對象,通常咱們使用默認配置就好,可是有些地方是要根據本身的狀況去作更改。好比,配置中的allowsInlineMediaPlayback
這個屬性,默認爲NO
,若是不作更改,網頁中內嵌的視頻就沒法正常播放。
有時咱們須要在User-Agent
添加一些額外的信息,這時就要更改默認的User-Agent
在使用UIWebView
的時候,可用以下代碼(在使用UIWebView
以前執行)全局更改User-Agent
:
1
2
3
4
5
6
7
8
9
10
|
// 獲取默認User-Agent
UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectZero];
NSString *oldAgent = [webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];
// 給User-Agent添加額外的信息
NSString *newAgent = [NSString stringWithFormat:@"%@;%@", oldAgent, @"extra_user_agent"];
// 設置global User-Agent
NSDictionary *dictionnary = [[NSDictionary alloc] initWithObjectsAndKeys:newAgent, @"UserAgent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dictionnary];
|
以上代碼是全局更改User-Agent
,也就是說,App
內全部的Web
請求的User-Agent
都被修改。替換爲WKWebView
後更改全局User-Agent
能夠繼續使用上面的一段代碼,或者改成用WKWebView
獲取默認的User-Agent
,代碼以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
self.wkWebView = [[WKWebView alloc] initWithFrame:CGRectZero];
// 獲取默認User-Agent
[self.wkWebView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id result, NSError *error) {
NSString *oldAgent = result;
// 給User-Agent添加額外的信息
NSString *newAgent = [NSString stringWithFormat:@"%@;%@", oldAgent, @"extra_user_agent"];
// 設置global User-Agent
NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:newAgent, @"UserAgent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dictionary];
}];
|
對比發現,這兩種方法並無本質的區別,一點小區別在於一個是用UIWebView
獲取的默認User-Agent
,一個是用WKWebView
獲取的默認User-Agent
。上面方法的缺點也是很明顯的,就是App
內全部Web
請求的User-Agent
所有被修改。
在iOS9
,WKWebView
提供了一個很是便捷的屬性去更改User-Agent
,就是customUserAgent
屬性。這樣使用起來不只方便,也不會全局更改User-Agent
,惋惜的是iOS9
纔有,若是適配iOS8
,仍是要使用上面的方法。
WKWebView
的相關的代理方法分別在WKNavigationDelegate
和WKUIDelegate
以及WKScriptMessageHandler
這個與JavaScript
交互相關的代理方法。
WKNavigationDelegate
: 此代理方法中除了原有的UIWebView
的四個代理方法,還增長了其餘的一些方法,具體可參考我下面給出的Demo
。WKUIDelegate
: 此代理方法在使用中最好實現,不然遇到網頁alert
的時候,若是此代理方法沒有實現,則不會出現彈框提示。WKScriptMessageHandler
: 此代理方法就是和JavaScript
交互相關,具體介紹參考下面的專門講解。由於咱們有個需求是在網頁下面在添加一個View
,用來展現此連接內容的相關評論。在使用UIWebView
的時候,作法很是簡單粗暴,在UIWebView
的ScrollView
後面添加一個自定義View
,而後根據View
的高度,在改變一下scrollView
的contentSize
屬性。覺得WKWebView
也能夠這樣簡單粗暴的去搞一下,結果卻並非這樣。
首先改變WKWebView
的scrollView
的contentSize
屬性,系統會在下一次幀率刷新的時候,再給你改變回原有的,這樣這條路就行不通了。我立刻想到了另外一個辦法,改變scrollView
的contentInset
這個系統倒不會在變化回原來的,自覺得完事大吉。後來過了兩天,發現有些頁面的部分區域的點擊事件沒法響應,百思不得其解,最後想到多是設置的contentInset
對其有了影響,事實上正是如此。查來查去,最後找到了一個解決辦法是,就是當頁面加載完成時,在網頁下面拼一個空白的div
,高度就是你添加的View
的高度,讓網頁多出一個空白區域,自定義的View
就添加在這個空白的區域上面。這樣就完美解決了此問題。具體可參考Demo
所寫,核心代碼以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
self.addView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, ScreenWidth, addViewHeight)];
self.addView.backgroundColor = [UIColor redColor];
[self.webView.scrollView addSubview:self.addView];
NSString *js = [NSString stringWithFormat:@"\
var appendDiv = document.getElementById(\"AppAppendDIV\");\
if (appendDiv) {\
appendDiv.style.height = %@+\"px\";\
} else {\
var appendDiv = document.createElement(\"div\");\
appendDiv.setAttribute(\"id\",\"AppAppendDIV\");\
appendDiv.style.width=%@+\"px\";\
appendDiv.style.height=%@+\"px\";\
document.body.appendChild(appendDiv);\
}\
", @(addViewHeight), @(self.webView.scrollView.contentSize.width), @(addViewHeight)];
[self.webView evaluateJavaScript:js completionHandler:nil];
|
HTTPS
已經愈來愈被重視,前面我也寫過一系列的HTTPS
的相關文章HTTPS從原理到應用(四):iOS中HTTPS實際使用當加載一些HTTPS
的頁面的時候,若是此網站使用的根證書已經內置到了手機中這些HTTPS
的連接能夠正常的經過驗證並正常加載。可是若是使用的證書(通常爲自建證書)的根證書並無內置到手機中,這時是連接是沒法正常加載的,必需要作一個權限認證。開始在UIWebView
的時候,是把請求存儲下來而後使用NSURLConnection
去從新發起請求,而後走NSURLConnection
的權限認證通道,認證經過後,在使用UIWebView
去加載這個請求。
在WKWebView
中,WKNavigationDelegate
中提供了一個權限認證的代理方法,這是權限認證更爲便捷。代理方法以下:
1
2
3
4
5
6
7
8
9
10
11
12
|
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if ([challenge previousFailureCount] == 0) {
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
} else {
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
} else {
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
}
|
這個方法比原來UIWebView
的認證簡單的多。可是使用中卻發現了一個很蛋疼的問題,iOS8
系統下,自建證書的HTTPS
連接,不調用此代理方法。查來查去,原來是一個bug
,在iOS9
中已經修復,這明顯就是無論iOS8
的狀況了,並且此方法也沒有標記在iOS9
中使用,這點讓我感到有點失望。這樣我就又想到了換回原來UIWebView
的權限認證方式,可是試來試去,發現也不能使用了。因此關於自建證書的HTTPS
連接在iOS8
下面使用WKWebView
加載,我沒有找到很好的辦法去解決此問題。這樣我不得已有些連接換回了HTTP
,或者在iOS8
下面在換回UIWebView
。若是你有解決辦法,也歡迎私信我,感激涕零。
WKWebView
和JavaScript
交互,在WKUserContentController.h
這個頭文件中- (void)addScriptMessageHandler:(id )scriptMessageHandler name:(NSString *)name;
這個方法的註釋中已經明確給出了交互辦法。使用起來卻是很是的簡單。建立WKWebView
的時候添加交互對象,並讓交互對象實現WKScriptMessageHandler
中的惟一的一個代理方法。具體的方式參考Demo中的使用。
1
2
3
4
5
|
// 添加交互對象
[config.userContentController addScriptMessageHandler:(id)self.ocjsHelper name:@"timefor"];
// 代理方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
|
JavaScript
調用Objective-C
的時候,使用window.webkit.messageHandlers.timefor.postMessage({code: '0001', functionName: 'getdevideId'}); Objective-C
自動對交互參數包裝成了WKScriptMessage
對象,其屬性body
則爲傳送過來的參數,name
爲添加交互對象的時候設置的名字,以此名字能夠過濾掉不屬於本身的交互方法。其中body
能夠爲NSNumber, NSString, NSDate, NSArray, NSDictionary, and NSNull。
而Objective-C
在回調JavaScript
的時候,不能像我原來在 Objective-C與JavaScript交互的那些事這篇文章中寫的那樣,JavaScript
傳過來一個匿名函數,Objective-C
這邊直接調用一下就完事。WKWebView
沒有辦法傳過來一個匿名函數,因此回調方式,要麼執行一段JavaScript
代碼,或者就是調用JavaScript
那邊的一個全局函數。通常是採用後者,至於Web
端雖然說暴露了一個全局函數,一樣能夠把這一點代碼處理的很優雅。Objective-C
傳給JavaScript
的參數,能夠爲Number, String, and Object
。參考以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 數字
NSString *js = [NSString stringWithFormat:@"globalCallback(%@)", number];
[self.webView evaluateJavaScript:js completionHandler:nil];
// 字符串
NSString *js = [NSString stringWithFormat:@"globalCallback(\'%@\')", string];
[self.webView evaluateJavaScript:js completionHandler:nil];
// 對象
NSString *js = [NSString stringWithFormat:@"globalCallback(%@)", @{@"name" : @"timefor"}];
[self.webView evaluateJavaScript:js completionHandler:nil];
// 帶返回值的JS函數
[self.webView evaluateJavaScript:@"globalCallback()" completionHandler:^(id result, NSError * _Nullable error) {
// 接受返回的參數,result中
}];
|
此文主要介紹了WKWebView使用中的注意點,通常也都是經常使用的,還有緩存等一些不是太經常使用的就沒有具體介紹。若是在其餘方面遇到問題,也歡迎你私信我共同探討進步。
此文的Demo地址:WKWebViewDemo 若是此文對你有所幫助,請給個star
吧。