WKWebView學習和添加進度條

占位圖.png

WKWebView自出現(xiàn)以來一直被人們所推崇,原因是他的優(yōu)點很多:

  • 更多的支持HTML5的特性,與JS交互更容易

  • 官方宣稱的高達60fps的滾動刷新率以及內(nèi)置手勢

  • 將UIWebViewDelegate與UIWebView拆分成了14類與3個協(xié)議,以前很多不方便實現(xiàn)的功能得以實現(xiàn)。

  • Safari相同的JavaScript引擎

  • 占用更少的內(nèi)存,加載速度快

但是我一直以來對WKWebView都不是太熟悉,所以最近抽時間學習了一下,并做了總結(jié)

WKWebView有兩個delegate,WKUIDelegate 和 WKNavigationDelegate。WKNavigationDelegate主要處理一些跳轉(zhuǎn)、加載處理操作,WKUIDelegate主要處理JS腳本,確認框,警告框等。因此WKNavigationDelegate更加常用。

WKWebview提供了API實現(xiàn)js交互 不需要借助JavaScriptCore或者webJavaScriptBridge(由于WKWebView是在一個單獨的進程中運行,我們無法獲取到 JSContext,所以我們無法使用 JSCore 這個強大的框架來進行交互)。使用WKWebViewConfiguration類中的一個屬性WKUserContentController,即userContentController,來實現(xiàn)js native交互。簡單的說就是先注冊約定好的方法,然后再調(diào)用。在 WKWebVeiw 中,我們使用我們有兩種方式來調(diào)用 JS,一種是使用 WKUserScript;另一種是直接調(diào)用 JS 字符串

然后列舉下里面所包含的類和協(xié)議

WKBackForwardList   之前訪問過的 web 頁面的列表,可以通過后退和前進動作來訪問到。
WKBackForwardListItem: webview 中后退列表里的某一個網(wǎng)頁。
WKFrameInfo: 包含一個網(wǎng)頁的布局信息。
WKNavigation: 包含一個網(wǎng)頁的加載進度信息。
WKNavigationAction: 包含可能讓網(wǎng)頁導航變化的信息,用于判斷是否做出導航變化。
WKNavigationResponse: 包含可能讓網(wǎng)頁導航變化的返回內(nèi)容信息,用于判斷是否做出導航變化。
WKPreferences: 概括一個 webview 的偏好設置。
WKProcessPool: 表示一個 web 內(nèi)容加載池。
WKUserContentController: 提供使用 JavaScript post 信息和注射 script 的方法。
WKScriptMessage: 包含網(wǎng)頁發(fā)出的信息。
WKUserScript: 表示可以被網(wǎng)頁接受的用戶腳本。WKWebViewConfiguration: 初始化 webview 的設置。
WKWindowFeatures: 指定加載新網(wǎng)頁時的窗口屬性。

協(xié)議

WKNavigationDelegate: 提供了追蹤主窗口網(wǎng)頁加載過程和判斷主窗口和子窗口是否進行頁面加載新頁面的相關(guān)方法。
WKScriptMessageHandler: 提供從網(wǎng)頁中收消息的回調(diào)方法。
WKUIDelegate: 提供用原生控件顯示網(wǎng)頁的方法回調(diào)。

協(xié)議方法

#pragma mark - WKNavigationDelegate  WKNavigationDelegate來追蹤加載過程
// 頁面開始加載時調(diào)用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation{
}
// 開始渲染頁面時調(diào)用,響應的內(nèi)容到達主頁面的時候響應,剛準備開始渲染頁面
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation{
}
// 頁面加載完成之后調(diào)用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation{
}
// 頁面加載失敗時調(diào)用
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation{
}
#pragma mark - WKNavigationDelegate  WKNavigtionDelegate來進行頁面跳轉(zhuǎn)
// 接收到服務器跳轉(zhuǎn)請求之后調(diào)用,接收到服務器跳轉(zhuǎn)請求即服務重定向時之后調(diào)用 
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation{
}
// 在收到響應后,決定是否跳轉(zhuǎn)。根據(jù)客戶端受到的服務器響應頭以及response相關(guān)信息來決定是否可以跳轉(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);
}
// 在發(fā)送請求之前,決定是否跳轉(zhuǎn),在這個方法里可以對頁面跳轉(zhuǎn)進行攔截處理
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
    //獲取請求的url路徑
    NSLog(@"%@",navigationAction.request.URL.absoluteString);
    // 遇到要做出改變的字符串
    NSString *subStr = @"www.baidu.com";
    if ([navigationAction.request.URL.absoluteString rangeOfString:subStr].location != NSNotFound) {
        //回調(diào)的URL中如果含有百度,就直接返回,也就是關(guān)閉了webView界面
        [self.navigationController  popViewControllerAnimated:YES];
    }
    //允許跳轉(zhuǎn)
    decisionHandler(WKNavigationActionPolicyAllow);
    //不允許跳轉(zhuǎn)
    //decisionHandler(WKNavigationActionPolicyCancel);
}
//需要響應身份驗證時調(diào)用 同樣在block中需要傳入用戶身份憑證
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler{
    //用戶身份信息
    NSURLCredential * newCred = [[NSURLCredential alloc] initWithUser:@"user123" password:@"123" persistence:NSURLCredentialPersistenceNone];
    //為 challenge 的發(fā)送方提供 credential
    [challenge.sender useCredential:newCred forAuthenticationChallenge:challenge];
    completionHandler(NSURLSessionAuthChallengeUseCredential,newCred);
}
    //進程被終止時調(diào)用(當 WKWebView 總體內(nèi)存占用過大,頁面即將白屏時會調(diào)用該方法)
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView{
}


#pragma mark - WKUIDelegate
// 創(chuàng)建一個新的WebView,解決點擊內(nèi)部鏈接沒有反應問題 
- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures{
    return [[WKWebView alloc]init];
}
//2.WebVeiw關(guān)閉(9.0中的新方法)
- (void)webViewDidClose:(WKWebView *)webView{
}

// 彈出一個輸入框(與JS交互的)
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * __nullable result))completionHandler{
    completionHandler(@"http");
}
// 顯示一個確認框(JS的)
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler{
    completionHandler(YES);
}
// 顯示一個JS的Alert(與JS交互)
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{
    NSLog(@"%@",message);
    completionHandler();
}

WKWebView與cookie

這里說下為什么在WKWebView要添加cookie:,其原因是因為WKWebView是在一個單獨的進程中運行,所以有時候登錄狀態(tài)會丟失,所以需要cookie.

1.在創(chuàng)建的時候存放到WKUserScript中進行添加cookie

//創(chuàng)建配置
WKWebViewConfiguration *webConfig = [[WKWebViewConfiguration alloc] init];
    // 設置偏好設置
    webConfig.preferences = [[WKPreferences alloc] init];
    // 默認為0
    webConfig.preferences.minimumFontSize = 10;
    //打開js交互   默認為YES
    webConfig.preferences.javaScriptEnabled = YES;
    //不通過用戶交互,是否可以打開窗口
    // 在iOS上默認為NO,表示不能自動通過窗口打開
   webConfig.preferences.javaScriptCanOpenWindowsAutomatically = NO;

    // web內(nèi)容處理池
    webConfig.processPool = [[WKProcessPool alloc] init];
    // 將所有cookie以document.cookie = 'key=value';形式進行拼接
    #warning 然而這里的單引號一定要注意是英文的,不要問我為什么告訴你這個(手動微笑)
    NSString *cookieValue = @"document.cookie = 'fromapp=ios';document.cookie = 'channel=appstore';";
    
    // 加cookie給h5識別,表明在ios端打開該地址
    WKUserContentController* userContentController = WKUserContentController.new;
    //下面一段也是原生吊JS的方法
    // source 就是我們要調(diào)用的 JS 函數(shù)
    // injectionTime 這個參數(shù)我們需要指定一個時間,在什么時候把我們在這段 JS 注入到 WebVeiw 中,它是一個枚舉值,WKUserScriptInjectionTimeAtDocumentStart 或者 WKUserScriptInjectionTimeAtDocumentEnd
    // MainFrameOnly 因為在 JS 中,一個頁面可能有多個 frame,這個參數(shù)指定我們的 JS 代碼是否只在 mainFrame 中生效
    WKUserScript * cookieScript = [[WKUserScript alloc]
                                   initWithSource: cookieValue
                                   injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
    [userContentController addUserScript:cookieScript];
    webConfig.userContentController = userContentController;

    WKWebView *wkWebView = [[WKWebView alloc] initWithFrame:frame configuration:webConfig];
//獲取web的標題
[webview addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:NULL];
//設置導航代理
        _webView.navigationDelegate = self;
        //[UIColor clearColor]
        _webView.backgroundColor = [UIColor orangeColor];
        //打開網(wǎng)頁間的 滑動返回
        _webView.allowsBackForwardNavigationGestures =YES;
        //滑動減速的速度
        _webView.scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;
        //禁止?jié)L動
        //_webView.scrollView.scrollEnabled = NO;
        //彈簧效果
        //_webView.scrollView.bounces = YES;

加載某個url的時候添加cookie

//如果WKWebView在加載url的時候需要添加cookie,需要先手動獲取當前NSHTTPCookieStorage中的所有cookie,然后將cookie放到NSMutableURLRequest請求頭中
- (void)loadRequestWithUrlString:(NSString *)urlString  withWeb:(WKWebView *)web{
    
    // 在此處獲取返回的cookie
    NSMutableDictionary *cookieDic = [NSMutableDictionary dictionary];
    
    NSMutableString *cookieValue = [NSMutableString stringWithFormat:@""];
    NSHTTPCookieStorage *cookieJar = [NSHTTPCookieStorage sharedHTTPCookieStorage];
    
    for (NSHTTPCookie *cookie in [cookieJar cookies]) {
        [cookieDic setObject:cookie.value forKey:cookie.name];
    }
    
    // cookie重復,先放到字典進行去重,再進行拼接
    for (NSString *key in cookieDic) {
        NSString *appendString = [NSString stringWithFormat:@"%@=%@;", key, [cookieDic valueForKey:key]];
        [cookieValue appendString:appendString];
    }
    
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
    [request addValue:cookieValue forHTTPHeaderField:@"Cookie"];
    
    [web loadRequest:request];
}

第二部分、添加進度條

#import <WebKit/WebKit.h>
@property (nonatomic, strong) WKWebView *webview;
@property (nonatomic, strong)UIProgressView *progressView;
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor whiteColor];
    self.webview = [[WKWebView alloc]initWithFrame:CGRectMake(0, 64, [[UIScreen mainScreen] bounds].size.width, [[UIScreen mainScreen] bounds].size.height)];
    NSString *urlString = @"https://www.baidu.com/";
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
    request.timeoutInterval = 15.0f;
    [self.webview loadRequest:request];
    [self.view addSubview:self.webview];
    //進度條初始化
    self.progressView = [[UIProgressView alloc] initWithFrame:CGRectMake(0, 0, [[UIScreen mainScreen] bounds].size.width, 1)];
    //設置進度條的高度,下面這句代碼表示進度條的寬度變?yōu)樵瓉淼?倍,高度變?yōu)樵瓉淼?.5倍.
    self.progressView.transform = CGAffineTransformMakeScale(1.0f, 2.0f);
    [self.webview addSubview:self.progressView];
    //    為進度條KVO
    [self.webview addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil];
    //添加監(jiān)測網(wǎng)頁標題title的觀察者
[self.webView addObserver:self
               forKeyPath:@"title"
                  options:NSKeyValueObservingOptionNew
                  context:nil];
}
// 計算wkWebView進度條
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (object == self.webview && [keyPath isEqualToString:@"estimatedProgress"]) {
        CGFloat newprogress = [[change objectForKey:NSKeyValueChangeNewKey] doubleValue];
        if (newprogress == 1) {
            self.progressView.hidden = YES;
            [self.progressView setProgress:0 animated:NO];
        }else {
            self.progressView.hidden = NO;
            [self.progressView setProgress:newprogress animated:YES];
        }
    }else  if([keyPath isEqualToString:@"title"]){
        self.navigationItem.title = _webView.title;
    }
}

// 記得取消監(jiān)聽
- (void)dealloc {
    [self.webview removeObserver:self forKeyPath:@"estimatedProgress"];
//移除觀察者
//[_webView removeObserver:self forKeyPath:NSStringFromSelector(@selector(estimatedProgress))];
//[_webView removeObserver:self forKeyPath:NSStringFromSelector(@selector(title))];
}

如果不想用progressView,那么我們可以用UIView自定義一個progressView

@property (nonatomic, strong)CALayer *progresslayer;
UIView *progress = [[UIView alloc]initWithFrame:CGRectMake(0, 64, CGRectGetWidth(self.view.frame), 3)];
    progress.backgroundColor = [UIColor clearColor];
    [self.view addSubview:progress];
    CALayer *layer = [CALayer layer];
    layer.frame = CGRectMake(0, 0, 0, 3);
    layer.backgroundColor = [UIColor orangeColor].CGColor;
    [progress.layer addSublayer:layer];
    self.progresslayer = layer;

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
    if ([keyPath isEqualToString:@"estimatedProgress"]) {
        self.progresslayer.opacity = 1;
        //不要讓進度條倒著走...有時候goback會出現(xiàn)這種情況
        if ([change[@"new"] floatValue] < [change[@"old"] floatValue]) {
            return;
        }
        self.progresslayer.frame = CGRectMake(0, 0, self.view.bounds.size.width * [change[@"new"] floatValue], 3);
        if ([change[@"new"] floatValue] == 1) {
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                self.progresslayer.opacity = 0;
            });
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                self.progresslayer.frame = CGRectMake(0, 0, 0, 3);
            });
        }
    }else{
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

第三部分、原生和 JS 交互

先說 JS 調(diào)用原生的方式

1.利用 WKUIDelegate 的三個代理方法對 JS 進行攔截
alert() 彈出個提示框,只能點確認無回調(diào)
confirm() 彈出個確認框(確認,取消),可以回調(diào),根據(jù)傳來的prompt字符串反解出數(shù)據(jù),判斷是否是所需要的攔截而非常規(guī)H5彈框
prompt() 彈出個輸入框,讓用戶輸入東西,可以回調(diào)

2.利用JS的上下文注入,可以用scriptMessageHandler注入,也可以用WKUserScript WKWebView的addUserScript方法,在加載時機注入
這個實現(xiàn)主要是依靠WKScriptMessageHandler協(xié)議類和WKUserContentController兩個類:WKUserContentController對象負責注冊JS方法,設置處理接收JS方法的代理,代理遵守WKScriptMessageHandler,實現(xiàn)捕捉到JS消息的回調(diào)方法
注意:遵守WKScriptMessageHandler協(xié)議,代理是由WKUserContentControl設置

//配置對象注入
[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"nativeObject"];
//移除對象注入
//[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"nativeObject"];

//蘋果WKWebView scriptMessageHandler注入 - 客戶端接收調(diào)用
-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    //message.name
    
    //1 解讀JS傳過來的JSValue  data數(shù)據(jù)
    NSDictionary *msgBody = message.body;
    //2 取出指令參數(shù),確認要發(fā)起的native調(diào)用的指令是什么
    //3 取出數(shù)據(jù)參數(shù),拿到JS傳過來的數(shù)據(jù)
    //4 根據(jù)指令調(diào)用對應的native方法,傳遞數(shù)據(jù)
}


//在 網(wǎng)頁的 js方法中
//window.webkit.messageHandlers.nativeObject.postMessage("")

這里需要注意一下,網(wǎng)頁執(zhí)行的那一行 js 代碼,最后一定要返回一個參數(shù),哪怕是一個空字符串也行,否則什么都不傳的話,原生方法是不會被執(zhí)行的

3.還可以用 decidePolicyForNavigationAction 對url 進行攔截判斷

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    //1 根據(jù)url,判斷是否是所需要的攔截的調(diào)用 判斷協(xié)議/域名
    NSString * urlStr = navigationAction.request.URL.absoluteString;
    NSLog(@"發(fā)送跳轉(zhuǎn)請求:%@",urlStr);
    //自己定義的協(xié)議頭
    NSString *htmlHeadString = @"github://";
    if (![urlStr hasPrefix:htmlHeadString]){
      //2 取出路徑,確認要發(fā)起的native調(diào)用的指令是什么
      //3 取出參數(shù),拿到JS傳過來的數(shù)據(jù)
      //4 根據(jù)指令調(diào)用對應的native方法,傳遞數(shù)據(jù)

      //確認攔截,拒絕WebView繼續(xù)發(fā)起請求
        decisionHandler(WKNavigationActionPolicyCancel);
    }else{
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"通過截取URL調(diào)用OC" message:@"你想前往我的Github主頁?" preferredStyle:UIAlertControllerStyleAlert];
        [alertController addAction:([UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        }])];
        [alertController addAction:([UIAlertAction actionWithTitle:@"打開" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
            NSURL * url = [NSURL URLWithString:[urlStr stringByReplacingOccurrencesOfString:@"github://callName_?" withString:@""]];
            [[UIApplication sharedApplication] openURL:url];
        }])];
        [self presentViewController:alertController animated:YES completion:nil];
        decisionHandler(WKNavigationActionPolicyAllow);
    }
    return YES;
}

原生調(diào)用JS

有兩種方式
1.evaluatingJavaScript
2.WKUserScript
這兩種方式的區(qū)別

evaluatingJavaScript 是在客戶端執(zhí)行這條代碼的時候立刻去執(zhí)行當條JS代碼
WKUserScript 是預先準備好JS代碼,當WKWebView加載Dom的時候,執(zhí)行當條JS代碼

第一種方式

//需要在客戶端用OC拼接字符串,拼出一個js代碼,傳遞的數(shù)據(jù)用json
NSString *paramsString = @"{data:xxx,data2:xxx}";
//拼接好的 js代碼         calljs('{data:xxx,data2:xxx}');
//其實我們拼接出來的js只是一行js代碼,當然無論多長多復雜的js代碼都可以用這個方式讓webview執(zhí)行
NSString* javascriptCommand = [NSString stringWithFormat:@"calljs('%@');", paramsString];
//OC調(diào)用js的方法,帶有回調(diào)    要求必須在主線程執(zhí)行JS
if ([[NSThread currentThread] isMainThread]) {
    [self.webView evaluateJavaScript:javascriptCommand completionHandler:nil];
} else {
    __strong typeof(self)strongSelf = self;
    dispatch_sync(dispatch_get_main_queue(), ^{
        [strongSelf.webView evaluateJavaScript:javascriptCommand completionHandler:nil];
    });
}

//在網(wǎng)頁端接收參數(shù)
//function calljs(data){
//    console.log(JSON.parse(data))
//    //1 識別客戶端傳來的數(shù)據(jù)
//    //2 對數(shù)據(jù)進行分析,從而調(diào)用或執(zhí)行其他邏輯
//}

第二種方式

//在loadurl之前使用   time是一個時機參數(shù),可選dom開始加載/dom加載完畢,2個時機進行執(zhí)行JS
//構(gòu)建userscript
WKUserScript *script = [[WKUserScript alloc]initWithSource:source injectionTime:time forMainFrameOnly:mainOnly];
WKUserContentController *userController = webView.userContentController;
//配置userscript
[userController addUserScript:script]
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容