一.前言
上一篇文章已經對WKWebView做了一個簡單的介紹,主要對它的一些方法和屬性做了一個簡單的介紹,今天看一下WKWebView的兩個協議:WKNavigationDelegate 和 WKUIDelegate。
二.WKNavigationDelegate
根據字面意思,它的作用是用於導航(navigation)的代理。其實里面定義了n多個方法,用於處理網頁接受、加載和導航請求等自定義的行為。直接拿下面的例子來看:
#pragma mark - WKWebView NavigationDelegate //WKNavigationDelegate - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { NSLog(@"是否允許這個導航"); decisionHandler(WKNavigationActionPolicyAllow); } - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler { // Decides whether to allow or cancel a navigation after its response is known. NSLog(@"知道返回內容之后,是否允許加載,允許加載"); decisionHandler(WKNavigationResponsePolicyAllow); } - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation { NSLog(@"開始加載"); self.progress.alpha = 1; [UIApplication sharedApplication].networkActivityIndicatorVisible = YES; } - (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation { NSLog(@"跳轉到其他的服務器"); } - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error { NSLog(@"網頁由於某些原因加載失敗"); self.progress.alpha = 0; [UIApplication sharedApplication].networkActivityIndicatorVisible = NO; } - (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation { NSLog(@"網頁開始接收網頁內容"); } - (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation { NSLog(@"網頁導航加載完畢"); [UIApplication sharedApplication].networkActivityIndicatorVisible = NO; self.title = webView.title; [webView evaluateJavaScript:@"document.title" completionHandler:^(id _Nullable ss, NSError * _Nullable error) { NSLog(@"----document.title:%@---webView title:%@",ss,webView.title); }]; self.progress.alpha = 0; } - (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error { NSLog(@"加載失敗,失敗原因:%@",[error description]); self.progress.alpha = 0; } - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView { NSLog(@"網頁加載內容進程終止"); } //- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler { // NSLog(@"receive"); //}
首先看一下(WKN1):
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { NSLog(@"是否允許這個導航"); decisionHandler(WKNavigationActionPolicyAllow); }
這個方法是加載網頁第一個執行的方法,因為它要確定是否允許或者取消加載這個導航。這里有一個枚舉WKNavigationActionPolicy,其結構如下:
typedef NS_ENUM(NSInteger, WKNavigationActionPolicy) { WKNavigationActionPolicyCancel, WKNavigationActionPolicyAllow, } API_AVAILABLE(macosx(10.10), ios(8.0));
這里的兩個枚舉確定了這個網頁是否加載,我們只需要在decisionHandler回調里面傳入相應的枚舉值即可。這里可以用來處理自己不允許加載的網頁,比如你的app里面的網頁很多,但是domain只有兩個,如果你只想加載這兩個domain里面的網頁,其他的domain不加載,那么可以在這里進行處理。(可以用來屏蔽移動或聯通運營商推送的網頁,讓其不在app中展示)
這里還有一個WKNavigationAction。它是一個導航動作, 包含了點擊之后的導航動作,我們做過濾的時候可以通過該動作決定是否允許加載。它有兩個關鍵的FrameInfo:
sourceFrame
targetFrame
他們都是WKFrameInfo的實例。該類包含了網頁加載的frame的信息。該類有一個重要的屬性:mainFrame。它是一個Bool值,用於標識該frame是不是當前網頁的主frame或者是子frame。舉個例子:
當我們第一次打開百度的時候,navigationAction是這樣的:
<WKNavigationAction: 0x100410fa0; navigationType = -1; syntheticClickType = 0;
request = <NSMutableURLRequest: 0x170019a10> { URL: https://www.baidu.com/ };
sourceFrame = (null); targetFrame = <WKFrameInfo: 0x100401200; isMainFrame = YES; request = (null)>>
它的request url是https://www.baidu.com/。它的sourceFrame是nil,也就是請求導航的frame是空的。它的targetFrame是:
<WKFrameInfo: 0x100401200; isMainFrame = YES; request = (null)>
這里是在當前的frame打開,也就是目的的frame。當我點擊新聞那個鏈接的時候,其navigationAction是這樣的:
<WKNavigationAction: 0x1004120b0; navigationType = -1; syntheticClickType = 0; request = <NSMutableURLRequest: 0x17001af30>
{ URL: http://m.news.baidu.com/news?fr=mohome&ssid=0&from=844b&uid=&pu=sz%401320_2001%2Cta%40iphone_1_10.2_3_602&bd_page_type=1
}; sourceFrame = (null); targetFrame = <WKFrameInfo: 0x100414ff0; isMainFrame = YES; request = <NSMutableURLRequest: 0x17001b080>
{ URL: https://www.baidu.com/ }>>
可以看到,此時的導航動作是要請求的
http://m.news.baidu.com/news?fr=mohome&ssid=0&from=844b&uid=&pu=sz%401320_2001%2Cta%40iphone_1_10.2_3_602&bd_page_type=1
也就是網頁版百度新聞的鏈接。此時它的sourceFrame是nil,它的targetFrame是:
<WKFrameInfo: 0x100414ff0; isMainFrame = YES; request = <NSMutableURLRequest: 0x17001b080> { URL: https://www.baidu.com/ }>
它的目標frame的request是baidu。其中的isMainFrame屬性是YES,說明是主的frame,所以還是在當前的網頁中打開一個新鏈接。
此時我們也許會遇到另外一種情況,就是sourceFrame不為空,但是targetFrame為空,這里如果targetFrame為空,那么這個就是新建一個window 導航。用我們自己的話來說就是重新打開了一個tab頁面。

就類似這樣,我點擊了PC版的網頁鏈接,然后新開了一個tab,這樣的話sourceFrame就是當前的百度,而targetFrame就是空的。此時(我用這個網頁測試)你會發現這個代理方法不執行了,好尷尬。。。。處理都不知道怎么處理了。由於新打開了tab,也就意味着我們的請求不在當前的網頁加載了,那么也無法調用到這個代理方法了。因此我們需要重新配置這個新打開的網頁,這里就會調用:
- (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration
forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures;
這個代理方法是WKUIDelegate的代理方法,可在下面查看。
接着就是開始加載(WKN2):
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
這個方法比較好理解,就是當網頁內容開始加載到web view的時候調用,這里的navigation沒有其他特殊含義,看一下WKNavigation這個類可以知道,他就是NSObject的一個子類,而且里面沒有任何新增的其他方法或者屬性。我想僅僅是為了名字上能夠好理解才這樣寫的吧。
再接着就是(WKN3):
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse
decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
這個代理方法了。我們知道了網頁是否允許加載,那么一旦不允許,那么這個加載過程就已經結束了,不會再執行其他的代理方法;如果允許,那么就會執行開始加載的代理方法,執行完開始加載的代理方法的時候再執行這個代理方法。
根據意思可知,它的作用就是要根據導航的返回信息來判斷是否加載網頁。我們首先打印出navigationResponse的信息:
<WKNavigationResponse: 0x100323e50; response = <NSHTTPURLResponse: 0x170226780> { URL: https://www.baidu.com/ } { status code: 200, headers { "Cache-Control" = "no-cache"; Connection = "keep-alive"; "Content-Encoding" = gzip; "Content-Length" = 20059; "Content-Type" = "text/html;charset=utf-8"; Date = "Mon, 27 Mar 2017 05:29:56 GMT"; Server = "bfe/1.0.8.18"; "Set-Cookie" = "H_WISE_SIDS=108266_100186_114821_114654_114743_109815_103550_114996_114701_112106_107314_114132_115245_115109_115056_115244_115043_114797_114513_114998_115227_114329_114534_115032_114276_114975_110085; path=/; domain=.baidu.com, BDSVRTM=182; path=/, __bsi=11762462753482193024_00_281_N_N_189_0303_C02F_N_N_Y_0; expires=Mon, 27-Mar-17 05:30:01 GMT; domain=www.baidu.com; path=/"; "Strict-Transport-Security" = "max-age=172800"; Traceid = 149059259607016350822661639119137312881; } }>
這個response有個屬性叫做forMainFrame,用於標識導航的frame是不是主frame。
navigationResponse的canShowMIMEType屬性用於表示WebKit是否能夠展示返回的MIME類型。
如果我們拿到了返回的信息,發現這些信息我們不需要,我們可以在此方法里面進行處理,然后決定是否加載該網頁。
接下來執行的是(WKN4):
- (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation;
這個代理方法是在網頁開始接受網絡內容的時候調用,也就是網絡內容開始要往網頁中加載。
再接着就是(WKN5):
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;
意思就是這個導航我們已經加載完成了。我的理解就是這個當前網頁加載完畢。
還有剩下的三個方法,一個加載網頁失敗的方法,一個接收服務器跳轉方法和一個網頁加載進程終止的方法。
網頁加載失敗方法:
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
當網頁由於error加載失敗就會調用這個代理方法,它會將具體的error信息給拋出,然后供開發者具體情況具體處理。這里的error code具體在NSURLError.h里面定義,具體為:
NS_ENUM(NSInteger) { NSURLErrorUnknown = -1, NSURLErrorCancelled = -999, NSURLErrorBadURL = -1000, NSURLErrorTimedOut = -1001, NSURLErrorUnsupportedURL = -1002, NSURLErrorCannotFindHost = -1003, NSURLErrorCannotConnectToHost = -1004, NSURLErrorNetworkConnectionLost = -1005, NSURLErrorDNSLookupFailed = -1006, NSURLErrorHTTPTooManyRedirects = -1007, NSURLErrorResourceUnavailable = -1008, NSURLErrorNotConnectedToInternet = -1009, NSURLErrorRedirectToNonExistentLocation = -1010, NSURLErrorBadServerResponse = -1011, NSURLErrorUserCancelledAuthentication = -1012, NSURLErrorUserAuthenticationRequired = -1013, NSURLErrorZeroByteResource = -1014, NSURLErrorCannotDecodeRawData = -1015, NSURLErrorCannotDecodeContentData = -1016, NSURLErrorCannotParseResponse = -1017, NSURLErrorAppTransportSecurityRequiresSecureConnection NS_ENUM_AVAILABLE(10_11, 9_0) = -1022, NSURLErrorFileDoesNotExist = -1100, NSURLErrorFileIsDirectory = -1101, NSURLErrorNoPermissionsToReadFile = -1102, NSURLErrorDataLengthExceedsMaximum NS_ENUM_AVAILABLE(10_5, 2_0) = -1103, // SSL errors NSURLErrorSecureConnectionFailed = -1200, NSURLErrorServerCertificateHasBadDate = -1201, NSURLErrorServerCertificateUntrusted = -1202, NSURLErrorServerCertificateHasUnknownRoot = -1203, NSURLErrorServerCertificateNotYetValid = -1204, NSURLErrorClientCertificateRejected = -1205, NSURLErrorClientCertificateRequired = -1206, NSURLErrorCannotLoadFromNetwork = -2000, // Download and file I/O errors NSURLErrorCannotCreateFile = -3000, NSURLErrorCannotOpenFile = -3001, NSURLErrorCannotCloseFile = -3002, NSURLErrorCannotWriteToFile = -3003, NSURLErrorCannotRemoveFile = -3004, NSURLErrorCannotMoveFile = -3005, NSURLErrorDownloadDecodingFailedMidStream = -3006, NSURLErrorDownloadDecodingFailedToComplete =-3007, NSURLErrorInternationalRoamingOff NS_ENUM_AVAILABLE(10_7, 3_0) = -1018, NSURLErrorCallIsActive NS_ENUM_AVAILABLE(10_7, 3_0) = -1019, NSURLErrorDataNotAllowed NS_ENUM_AVAILABLE(10_7, 3_0) = -1020, NSURLErrorRequestBodyStreamExhausted NS_ENUM_AVAILABLE(10_7, 3_0) = -1021, NSURLErrorBackgroundSessionRequiresSharedContainer NS_ENUM_AVAILABLE(10_10, 8_0) = -995, NSURLErrorBackgroundSessionInUseByAnotherProcess NS_ENUM_AVAILABLE(10_10, 8_0) = -996, NSURLErrorBackgroundSessionWasDisconnected NS_ENUM_AVAILABLE(10_10, 8_0)= -997, };
這里有個小小的問題需要說明一下:
當我是用這個URL來進行加載的時候。我點擊進去一個連接,然后再返回上一個頁面的時候,發現會執行加載失敗,查看原因是-999,也就是NSURLErrorCancelled。我理解的應該是因為之前上一個網頁已經加載過了,再次加載的時候發現已經加載過了,所以這次返回上一個頁面會報一個加載取消,應該是webview發現要加載的這個網頁之前已經加載過,然后就取消了這個加載,加載了緩存。
另外兩個代理方法:
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
和
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView API_AVAILABLE(macosx(10.11), ios(9.0));
前者重定向跳轉我們也許會經常遇到,但是一般不怎么去處理其內容,這里就不在介紹了,后者進程終止這個我也不太清楚怎么去觸發,也就不再介紹了。
接下來就來看一下各個代理方法的執行順序(直接跑百度):
2017-03-27 14:16:22.028051 WKWebViewDemo[561:39556] 是否允許這個導航 2017-03-27 14:16:22.028486 WKWebViewDemo[561:39556] 開始加載 2017-03-27 14:16:24.102160 WKWebViewDemo[561:39556] 知道返回內容之后,是否允許加載,允許加載 2017-03-27 14:16:24.106276 WKWebViewDemo[561:39556] 網頁開始接收網頁內容 2017-03-27 14:16:29.156518 WKWebViewDemo[561:39556] 網頁導航加載完畢 2017-03-27 14:16:29.177006 WKWebViewDemo[561:39556] ----document.title:百度一下---webView title:百度一下
這就是順序了。
忘了還有一個身份驗證的:
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler;
具體沒做了解,以后如果用到會進一步補充,這里感興趣的也可以自己看一下。
二.WKUIDelegate
該協議的代理方法的作用是使用原生的用戶界面來代表網頁。 說白了就是我們去手動寫新加載頁面的UI、alert框的樣式、對話框的樣式等等。直接拿下面的例子來看:
#pragma mark - WKWebView WKUIDelegate
- (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {
NSLog(@"創建一個新的webView");
if (!navigationAction.targetFrame.isMainFrame) {
[webView loadRequest:navigationAction.request];
}
return nil;
}
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"alert" message:message preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"確定1" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
completionHandler();
}]];
[self presentViewController:alert animated:YES completion:nil];
}
- (void)webViewDidClose:(WKWebView *)webView {
}
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler {
completionHandler(YES);
}
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler {
completionHandler(@"oc對象");
}
- (BOOL)webView:(WKWebView *)webView shouldPreviewElement:(WKPreviewElementInfo *)elementInfo {
return YES;
}
- (void)webView:(WKWebView *)webView commitPreviewingViewController:(UIViewController *)previewingViewController {
NSLog(@"Called when the user performs a pop action on the preview.");
}
首先來看一下:
- (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration
forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures;
在上面也提到了,如果我們新打開了一個tab,就會調用此方法。如果我們打開tab但是沒有實現這個方法,那么網頁就會取消這個導航,也就是沒有做出任何反應。所以使用WKWebView的時候一定要記得對這個方法進行處理,如果沒有處理也許就會導致點擊網頁沒有任何反應。此時可以這樣處理:
- (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures { NSLog(@"創建一個新的webView"); if (!navigationAction.targetFrame.isMainFrame) { [webView loadRequest:navigationAction.request]; } return nil; }
這樣做就是說如果是新打開的網頁,那么我們可以直接在當前網頁去加載要load的請求,然后返回nil,這樣就是在當前的網頁去打開新的tab頁面了。也可以直接判斷targetFrame是否空來進行loadRequest。
當我們的網頁中調用alert()方法的時候,我們就要去實現:
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler;
如果我們沒有實現這個方法,那么alert是沒有辦法彈出來的(我這邊測試的是彈不出來)。因此我們可以這樣實現此代理方法:
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler { UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"alert" message:message preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"確定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { completionHandler(); }]]; [self presentViewController:alert animated:YES completion:nil]; }
我們將alert的UI設置為我們系統的UIAlertController。這里的message就是要alert出來的數據信息。
接下來是:
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler { completionHandler(YES); }
此方法的作用是展示一個js確認框。這里的completionHandler的回調是確認框的yes or no。例如一個網頁的按鈕處理操作如下:
// confirm選擇框
function firm() {
var r=confirm("Press a button")
if (r==true)
{
document.write("You pressed OK!")
}
else
{
document.write("You pressed Cancel!")
}
}
如果我們傳入的是YES,那么就會執行“You pressed OK”,否則就是執行“You pressed Cancel”。
接下來是:
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt
defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler;
這里如果我們不去手動實現該方法,那么點擊的動作就像我們點擊取消,是一樣的效果。
例如 在HTML中button的click方法如下:
function prom() {
var result = prompt("演示一個帶輸入的對話框", "請輸入內容");
if(result) {
alert("謝謝使用,你輸入的是:" + result)
}
}
那么這個result就是我們在上面的代理方法里面傳入的result。例如:
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler {
completionHandler(@"oc對象");
}
那么他就會彈出:“謝謝使用,你輸入的是oc對象”。
其實這里我們可以對上面的三個代理方法進行深度定制UI,那么就能按照我們原生的UI顯示出來效果。
當我們設置WKWebView的屬性allowsLinkPreview為YES的時候,那么我們就可以進行3D touch預覽。默認的值是NO。如果我們設置YES,但是:
- (BOOL)webView:(WKWebView *)webView shouldPreviewElement:(WKPreviewElementInfo *)elementInfo {
return NO;
}
此代理方法中返回NO,那么依然無法展示預覽視圖,因為該代理方法的作用就是決定是否允許加載預覽視圖。(safari默認是支持3D touch預覽的)。而:
- (nullable UIViewController *)webView:(WKWebView *)webView previewingViewControllerForElement:(WKPreviewElementInfo *)elementInfo defaultActions:(NSArray<id <WKPreviewActionItem>> *)previewActions { return nil; }
這個方法就是用於我們自己定義預覽界面。另外還有:
- (void)webViewDidClose:(WKWebView *)webView API_AVAILABLE(macosx(10.11), ios(9.0));
和
- (void)webView:(WKWebView *)webView commitPreviewingViewController:(UIViewController *)previewingViewController API_AVAILABLE(ios(10.0));
前者是DOM window成功關閉的時候調用。后者是當用戶在預覽中執行彈出操作時調用。
四.總結
簡單就對WKWebView的代理介紹這么多,在后面如果有其他需要補充的再補充。
五.附
源碼地址。
