IOS進階之WKWebView


前言


Xcode8發布以后,編譯器開始不支持IOS7,所以很多應用在適配IOS10之后都不在適配IOS7了,其中包括了很多大公司,網易新聞,滴滴出行等。因此,我們公司的應用也打算淘汰IOS7。

支持到IOS8,第一個要改的自然是用WKWebView替換原來的UIWebView。WKWebView有很多明顯優勢:

  • 更多的支持HTML5的特性

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

  • 將UIWebViewDelegate與UIWebView拆分成了14類與3個協議,以前很多不方便實現的功能得以實現。文檔

  • Safari相同的JavaScript引擎

  • 占用更少的內存

UIWebView


UIWebView

WKWebView


WKWebView

因此,使用WkWebview替換UIWebView還是很有必要的。

基本使用方法


WKWebView有兩個delegate,WKUIDelegateWKNavigationDelegate。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(); }

OC與JS交互


WKWebview提供了API實現js交互 不需要借助JavaScriptCore或者webJavaScriptBridge。使用WKUserContentController實現js native交互。簡單的說就是先注冊約定好的方法,然后再調用。

JS調用OC方法

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配套出現,否則會造成內存泄漏。
  • h5只能傳一個參數,如果需要多個參數就需要用字典或者json組裝。

oc調用JS方法

代碼如下:

- (void)webView:(WKWebView *)tmpWebView didFinishNavigation:(WKNavigation *)navigation{ //say()是JS方法名,completionHandler是異步回調block [webView evaluateJavaScript:@"say()" completionHandler:^(id _Nullable result, NSError * _Nullable error) { NSLog(@"%@",result); }]; }

h5代碼同上。

WebViewJavascriptBridge


一般來說,一個好的UI總有一個大神會開發出一個好的第三方封裝框架。WebViewJavascriptBridge的作者也做了一套支持WKWebView與JS交互的第三方框架:WKWebViewJavascriptBridge。

主要方法如下:

//初始化方法 + (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下來使用。



文/o翻滾的牛寶寶o(簡書作者)
原文鏈接:http://www.jianshu.com/p/4fa8c4eb1316
著作權歸作者所有,轉載請聯系作者獲得授權,並標注“簡書作者”。
 
 
 

iOS8之后,蘋果推出了WebKit這個框架,用來替換原有的UIWebView,新的控件優點多多,不一一敘述。由於一直在適配iOS7,就沒有去替換,現在仍掉了iOS7,以為很簡單的就替換過來了,然而在替換的過程中,卻遇到了很多坑。還有一點就是原來寫過一篇文章 Objective-C與JavaScript交互的那些事以為年代久遠的UIWebView已經作古,可這篇文章現在依然有一定的閱讀量。所以在決定在續一篇此文,以引導大家轉向WKWebView,並指出自己踩過的坑,讓大家少走彎路。

此篇文章的邏輯圖

111192353-3b1b2a629853d8b2

WKWebView使用

WKWebView簡單介紹

使用及注意點

WKWebView只能用代碼創建,而且自身就支持了右滑返回手勢allowsBackForwardNavigationGestures和加載進度estimatedProgress等一些UIWebView不具備卻非常好用的屬性。在創建的時候,指定初始化方法中要求傳入一個WKWebViewConfiguration對象,一般我們使用默認配置就好,但是有些地方是要根據自己的情況去做更改。比如,配置中的allowsInlineMediaPlayback這個屬性,默認為NO,如果不做更改,網頁中內嵌的視頻就無法正常播放。

更改User-Agent

有時我們需要在User-Agent添加一些額外的信息,這時就要更改默認的User-Agent在使用UIWebView的時候,可用如下代碼(在使用UIWebView之前執行)全局更改User-Agent

以上代碼是全局更改User-Agent,也就是說,App內所有的Web請求的User-Agent都被修改。替換為WKWebView后更改全局User-Agent可以繼續使用上面的一段代碼,或者改為用WKWebView獲取默認的User-Agent,代碼如下:

對比發現,這兩種方法並沒有本質的區別,一點小區別在於一個是用UIWebView獲取的默認User-Agent,一個是用WKWebView獲取的默認User-Agent。上面方法的缺點也是很明顯的,就是App內所有Web請求的User-Agent全部被修改。

iOS9WKWebView提供了一個非常便捷的屬性去更改User-Agent,就是customUserAgent屬性。這樣使用起來不僅方便,也不會全局更改User-Agent,可惜的是iOS9才有,如果適配iOS8,還是要使用上面的方法。

WKWebView的相關的代理方法

WKWebView的相關的代理方法分別在WKNavigationDelegateWKUIDelegate以及WKScriptMessageHandler這個與JavaScript交互相關的代理方法。

  • WKNavigationDelegate: 此代理方法中除了原有的UIWebView的四個代理方法,還增加了其他的一些方法,具體可參考我下面給出的Demo
  • WKUIDelegate: 此代理方法在使用中最好實現,否則遇到網頁alert的時候,如果此代理方法沒有實現,則不會出現彈框提示。
  • WKScriptMessageHandler: 此代理方法就是和JavaScript交互相關,具體介紹參考下面的專門講解。

WKWebView使用過程中的坑

WKWebView下面添加自定義View

因為我們有個需求是在網頁下面在添加一個View,用來展示此鏈接內容的相關評論。在使用UIWebView的時候,做法非常簡單粗暴,在UIWebViewScrollView后面添加一個自定義View,然后根據View的高度,在改變一下scrollViewcontentSize屬性。以為WKWebView也可以這樣簡單粗暴的去搞一下,結果卻並不是這樣。

首先改變WKWebViewscrollViewcontentSize屬性,系統會在下一次幀率刷新的時候,再給你改變回原有的,這樣這條路就行不通了。我馬上想到了另一個辦法,改變scrollViewcontentInset這個系統倒不會在變化回原來的,自以為完事大吉。后來過了兩天,發現有些頁面的部分區域的點擊事件無法響應,百思不得其解,最后想到可能是設置的contentInset對其有了影響,事實上正是如此。查來查去,最后找到了一個解決辦法是,就是當頁面加載完成時,在網頁下面拼一個空白的div,高度就是你添加的View的高度,讓網頁多出一個空白區域,自定義的View就添加在這個空白的區域上面。這樣就完美解決了此問題。具體可參考Demo所寫,核心代碼如下:

 

WKWebView加載HTTPS的鏈接

HTTPS已經越來越被重視,前面我也寫過一系列的HTTPS的相關文章HTTPS從原理到應用(四):iOS中HTTPS實際使用當加載一些HTTPS的頁面的時候,如果此網站使用的根證書已經內置到了手機中這些HTTPS的鏈接可以正常的通過驗證並正常加載。但是如果使用的證書(一般為自建證書)的根證書並沒有內置到手機中,這時是鏈接是無法正常加載的,必須要做一個權限認證。開始在UIWebView的時候,是把請求存儲下來然后使用NSURLConnection去重新發起請求,然后走NSURLConnection的權限認證通道,認證通過后,在使用UIWebView去加載這個請求。

WKWebView中,WKNavigationDelegate中提供了一個權限認證的代理方法,這是權限認證更為便捷。代理方法如下:

這個方法比原來UIWebView的認證簡單的多。但是使用中卻發現了一個很蛋疼的問題,iOS8系統下,自建證書的HTTPS鏈接,不調用此代理方法。查來查去,原來是一個bug,在iOS9中已經修復,這明顯就是不管iOS8的情況了,而且此方法也沒有標記在iOS9中使用,這點讓我感到有點失望。這樣我就又想到了換回原來UIWebView的權限認證方式,但是試來試去,發現也不能使用了。所以關於自建證書的HTTPS鏈接在iOS8下面使用WKWebView加載,我沒有找到很好的辦法去解決此問題。這樣我不得已有些鏈接換回了HTTP,或者在iOS8下面在換回UIWebView。如果你有解決辦法,也歡迎私信我,感激不盡。

WKWebView和JavaScript交互

WKWebViewJavaScript交互,在WKUserContentController.h這個頭文件中- (void)addScriptMessageHandler:(id )scriptMessageHandler name:(NSString *)name;這個方法的注釋中已經明確給出了交互辦法。使用起來倒是非常的簡單。創建WKWebView的時候添加交互對象,並讓交互對象實現WKScriptMessageHandler中的唯一的一個代理方法。具體的方式參考Demo中的使用。

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。參考如下:

 

總結

此文主要介紹了WKWebView使用中的注意點,一般也都是常用的,還有緩存等一些不是太常用的就沒有具體介紹。如果在其他方面遇到問題,也歡迎你私信我共同探討進步。
此文的Demo地址:WKWebViewDemo 如果此文對你有所幫助,請給個star吧。

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM