慣例先看效果圖(后附demo地址)
??在iOS項目開發(fā)中,絕大多數(shù)功能都是我們原生開發(fā)的,但是像一些諸如用戶協(xié)議說明,公司介紹构舟,App內(nèi)的活動頁以及引用的其他網(wǎng)頁都需要借助web頁面來實現(xiàn),有的頁面簡單堵幽,只需要加載一個簡單的url即可狗超,有的頁面則需要用到OC和JS的交互來實現(xiàn)。
???提到WebView朴下,我們可能首先想起UIWebView努咐,在iOS8.0之前,都是用此來加載網(wǎng)頁殴胧,但是當(dāng)我們打開Apple的開發(fā)文檔(在此插一句題外話渗稍,當(dāng)我們開發(fā)的時候肯定會遇到一些問題佩迟,就拿這個UIWebView來說,我們想知道它加載網(wǎng)頁的方法竿屹,因此我們可能首先想到的就是去百度一下看看报强,有時候卻發(fā)現(xiàn)找來找去,不同的人有不同的寫法甚至是寫的并不對拱燃,或者說的并不明白秉溉,這時候我建議還是查看Apple的官方文檔,我們要習(xí)慣看Apple官方文檔碗誉,因為不僅權(quán)威而且還原汁原味召嘶。)發(fā)現(xiàn)UIWebView是在UIKit下的Content Views中。打開UIWebView后哮缺,有個提示:
In apps that run in iOS 8 and later, use the
WKWeb<wbr style="margin-top: 0px; margin-bottom: 0px;">View
class instead of usingUIWebView
. Additionally, consider setting theWKPreferences
propertyjava<wbr style="margin-top: 0px;">Script<wbr style="margin-bottom: 0px;">Enabled
toNO
if you render files that are not supposed to run JavaScript.
意思是說在iOS 8.0之后用WKWebView來替代UIWebView弄跌,此外WKWebView對于和JS的交互相比較UIWebView而言還需要不同的設(shè)置。蘋果在此讓我們用WKWebView來替代UIWebView尝苇,我們再來看下WKWebView铛只,通過Apple開發(fā)文檔發(fā)現(xiàn)有一個專門的WebKit,并不是在UIKit中糠溜。
UIWebView是在UIKit下
WKWebView是在WebKit下
???UIWebView就這么被替代了格仲,相信很多開發(fā)者還是很習(xí)慣UIWebView來開發(fā)的,因為使用方法相對簡單诵冒,但是相較于UIWebView,WKWebView更強大谊惭,他有什么新特性呢汽馋?
1.更多的支持HTML5的特性
2.官方宣稱的高達60fps的滾動刷新率以及內(nèi)置手勢
3.Safari相同的JavaScript引擎
4.將UIWebViewDelegate與UIWebView拆分成了14類與3個協(xié)議(官方文檔說明)
5.另外用的比較多的,增加加載進度屬性:estimatedProgress
哇圈盔,一看這么多優(yōu)點豹芯。。驱敲。這些優(yōu)點都是相對于UIWebView而言的铁蹈,真是沒有比較就沒有傷害。對于上面官方說的WKWebView的新特性众眨,有的開發(fā)者專門那UIWebView和WKWebView去測試了握牧,確實發(fā)現(xiàn)WKWebView比UIWebView在內(nèi)存上和加載速度上明顯更勝一籌,在實際開開發(fā)中娩梨,我們也能明顯感覺得到沿腰,加載同樣一個網(wǎng)頁,WKWebView比UIWebView相應(yīng)速度明顯快很多狈定。
對于UIWebView的使用方法颂龙,在這里就不多贅述了习蓬,Apple開發(fā)文檔有詳細(xì)的說明也比較簡單章咧,本文的重點是在我們開發(fā)中封裝一個公用的WebView伺帘,因此我們的主角還是WKWebView,當(dāng)然蓄坏,考慮到系統(tǒng)適配企巢,我們?nèi)匀豢梢允褂肬IWebView枫慷,但是現(xiàn)在運行iOS8.0的設(shè)備已經(jīng)很少了吧,如果要是為了考慮到自己項目的兼容性包斑,可以做一個系統(tǒng)版本的適配流礁。再次我們就以WKWebView為主。
開始前的思考
在開始之前罗丰,我們先想一下我們的Web請求一般會有怎樣的應(yīng)用場景
1.加載簡單的一個頁面 無附加操作
2.原生(OC)和web頁面(JS)交互神帅;包括JS調(diào)用OC和OC執(zhí)行JS代碼兩種方式
其中第一點很好實現(xiàn),基本不用多做處理萌抵,第二點找御,OC和JS交互就比較復(fù)雜了,首先我們先對WKWebView初始化:
- (WKWebView* )wkWebView {
if (!_wkWebView) {
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc]init];
config.preferences = [[WKPreferences alloc]init];
config.allowsInlineMediaPlayback = YES;
config.selectionGranularity = YES;
//自定義配置绍填,一般用于js調(diào)用oc方法(OC攔截URL中的數(shù)據(jù)做自定義操作)
WKUserContentController *userContentController = [[WKUserContentController alloc] init];
[userContentController addScriptMessageHandler:self name:@"backPresent"];
//JS注入 向網(wǎng)頁中添加自己的JS方法
NSString *cookie = self.cookies;
WKUserScript *cookieScript = [[WKUserScript alloc] initWithSource:cookie injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[userContentController addUserScript:cookieScript];
config.userContentController = userContentController;
_wkWebView = [[WKWebView alloc]initWithFrame:CGRectMake(0, NAVI_HEIGHT, self.view.bounds.size.width, self.view.bounds.size.height - NAVI_HEIGHT) configuration:config];
_wkWebView.navigationDelegate = self;
_wkWebView.UIDelegate = self;
//添加此屬性可觸發(fā)側(cè)滑返回上一網(wǎng)頁與下一網(wǎng)頁操作
_wkWebView.allowsBackForwardNavigationGestures = YES;
return _wkWebView;
}
WKWebViewConfiguration
在初始化方法中有一個必傳參數(shù)WKWebViewConfiguration,我們首先來看一下這個WKWebViewConfiguration是什么鬼霎桅,Apple開發(fā)文檔是這么說的:
Overview
Using the WKWebViewConfiguration class, you can determine how soon a webpage is rendered, how media playback is handled, the granularity of items that the user can select, and many other options.
WKWebViewConfiguration is only used when a web view is first initialized. You cannot use this class to change the web view's configuration after it has been created.
??說白了,通過這個類我們可以設(shè)置WKWebView的網(wǎng)頁渲染時間讨永,網(wǎng)頁媒體文件處理方式以及一些WKWebView的基本設(shè)置滔驶,只能在初始化的時候設(shè)置。也就是說通過設(shè)置WKWebViewConfiguration我們可以配置WKWebView的web頁面的處理方式卿闹。除了WKWebViewConfiguration之外揭糕,還有一個WKScriptMessageHandler類需要我們注意。
WKScriptMessageHandler
A class conforming to the WKScriptMessageHandler protocol provides a method for receiving messages from JavaScript running in a webpage.
其實就是一個遵循的協(xié)議锻霎,他的作用是能把JS的消息發(fā)送給OC
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
這個協(xié)議中有兩個參數(shù)一個是WKUserContentController著角,一個是WKScriptMessage,WKUserContentController負(fù)責(zé)將WKScriptMessage調(diào)度出來,因此要是實現(xiàn)JS調(diào)用OC旋恼,就要遵循這個協(xié)議并實現(xiàn)這個方法吏口。而且還要將WKUserContentController和WKScriptMessage在初始化時設(shè)置到WKWebViewConfiguration中去。
WKUserContentController
A WKUserContentController object provides a way for JavaScript to post messages and inject user scripts to a web view.
WKUserContentController有兩個核心方法:
//添加JS調(diào)用OC的橋梁冰更,這里的name就是WKScriptMessage中的name产徊。
- (void)addScriptMessageHandler:(id<WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
//向請求的頁面中注入JS代碼,如果需要在請求的頁面中加入自己的JS代碼蜀细,用此方法囚痴。
- (void)addUserScript:(WKUserScript *)userScript;
WKScriptMessage
WKScriptMessage就是JS通知OC時候攜帶的數(shù)據(jù),其中常用的屬性有
1.name
//name就是對應(yīng)中的name
- (void)addScriptMessageHandler:(id<WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
2.body 攜帶JS傳送的數(shù)據(jù)
WKUserScript
WKUserScript主要作用就是攜帶正確可執(zhí)行的JS代碼然后注入到要請求的webView中审葬,有一個初始化方法
- (instancetype)initWithSource:(NSString *)source injectionTime:(WKUserScriptInjectionTime)injectionTime forMainFrameOnly:(BOOL)forMainFrameOnly;
其中WKUserScriptInjectionTime是個枚舉類型深滚,以供選擇合適執(zhí)行JS代碼奕谭。
typedef enum WKUserScriptInjectionTime : NSInteger {
WKUserScriptInjectionTimeAtDocumentStart,//開始時注入
WKUserScriptInjectionTimeAtDocumentEnd //結(jié)束時注入
} WKUserScriptInjectionTime;
熟悉了以上幾個類,我們就可以初始化一個WKWebView并且配置我們用以O(shè)C和JS交互的一些配置痴荐,在此我使用了懶加載的方式:
- (WKWebView* )wkWebView {
if (!_wkWebView) {
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc]init];
config.preferences = [[WKPreferences alloc]init];
config.allowsInlineMediaPlayback = YES;
config.selectionGranularity = YES;
//自定義配置血柳,一般用于js調(diào)用oc方法(OC攔截URL中的數(shù)據(jù)做自定義操作)
WKUserContentController *userContentController = [[WKUserContentController alloc] init];
[userContentController addScriptMessageHandler:self name:@"backPresent"];
//JS注入 向網(wǎng)頁中添加自己的JS方法
NSString *cookie = self.cookies;
WKUserScript *cookieScript = [[WKUserScript alloc] initWithSource:cookie injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[userContentController addUserScript:cookieScript];
config.userContentController = userContentController;
_wkWebView = [[WKWebView alloc]initWithFrame:CGRectMake(0, NAVI_HEIGHT, self.view.bounds.size.width, self.view.bounds.size.height - NAVI_HEIGHT) configuration:config];
_wkWebView.navigationDelegate = self;
_wkWebView.UIDelegate = self;
//添加此屬性可觸發(fā)側(cè)滑返回上一網(wǎng)頁與下一網(wǎng)頁操作
_wkWebView.allowsBackForwardNavigationGestures = YES;
//下拉刷新
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 10.0 && _canDownRefresh) {
_wkWebView.scrollView.refreshControl = self.refreshControl;
}
//進度監(jiān)聽
[_wkWebView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:NULL];
}
return _wkWebView;
}
??在這段代碼中,我們首先設(shè)置了WKWebView的基本配置WKWebViewConfiguration生兆,然后再配置WKUserContentController难捌,將我們要JS調(diào)用OC的方法實現(xiàn),如果還需要向JS中注入我們添加的JS方法鸦难,則再配置WKUserScript根吁,完成這些之后,WKWebViewConfiguration配置就基本完成合蔽,然后我們初始化WKWebView并給他一個frame击敌。下面我們再看下WKWebView的兩個代理。
??在使用UIWebView的時候拴事,我們只設(shè)置UIWebViewDelegate就好了沃斤,在這個協(xié)議中包含了很多WebView的方法,包括加載刃宵,加載完成等衡瓶,而在WKWebView中,蘋果將其分解為WKNavigationDelegate和WKUIDelegate兩部分牲证,其實個人感覺這樣分下來更清晰哮针,我們知道一個WebView的組件加載網(wǎng)頁的時候,一般就兩個方面:
1.WebView對于網(wǎng)頁的渲染坦袍,這更多的是體現(xiàn)在UI上的提現(xiàn)诚撵,沒有很多頁面邏輯的處理。
2.WebView對于網(wǎng)頁的加載键闺,包括開始加載,加載完成澈驼,以及OC與JS的交互辛燥,這一層更多的是體現(xiàn)的是原生與Web頁面的交互,包含更多的交互缝其。
基于這兩點挎塌,我們發(fā)現(xiàn)WKUINavigationDelegate和WKUIDelegate這兩個代理很好的將UI和加載交互分開來了,讓我們可以更專注UI和交互不同模塊内边。
WKUINavigationDelegate
這個代理中包含很多方法榴都,我們看下比較常用的。
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation;
見名知意漠其,這個方法是當(dāng)webView加載web頁面的時候調(diào)用的嘴高。當(dāng)開始加載頁面時竿音,我們可以在這里做想做的操作。
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation;
與上面方法相反拴驮,當(dāng)頁面加載完成時會調(diào)用此方法春瞬。
- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error;
加載頁面出錯時調(diào)動,我們可以在此做進一步操作套啤,比如重新加載
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
這個方法決定返回方法是否允許加載宽气,其中有一個block參數(shù)來決定我們是否允許加載,WKNavigationResponsePolicy是一個枚舉類型
typedef enum WKNavigationResponsePolicy : NSInteger {
WKNavigationResponsePolicyCancel, // 不允許加載
WKNavigationResponsePolicyAllow //允許加載
} WKNavigationResponsePolicy;
WKUIDelegate
這個代理中的方法潜沦,最常用的就是Displaying UI Panels下的這三種提示框的加載:
//加載一個提示框
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler;
//確認(rèn)框
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler;
輸入框
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *result))completionHandler;
此外還有創(chuàng)建新的WebView和關(guān)閉WebView的方法萄涯,還有關(guān)于ForceTouch的支持方法,在文檔中都有詳細(xì)的說明唆鸡。
了解這些以后涝影,我們就基本可以創(chuàng)建出一個WKWebView,而且能實現(xiàn)與OC與JS的一些交互喇闸,通過設(shè)置他的代理方法來實現(xiàn)加載進度控制等袄琳,不過在WebView中,我們還會有點擊下一頁或者返回上一頁燃乍,或者對于加載一個進度的展示等需求唆樊,這些我們也要考慮在內(nèi)。
我們來看下WKWebView中有哪些屬性和方法刻蟹。
Property
title 標(biāo)題
URL 當(dāng)前url
scrollView
estimatedProgress 加載進度 浮點型
allowsBackForwardNavigationGestures 手勢支持
Method
// HTML加載方法
- (WKNavigation *)loadHTMLString:(NSString *)string baseURL:(NSURL *)baseURL;
//重新加載
- (WKNavigation *)reload;
//停止加載
- (void)stopLoading;
//上一頁(不是關(guān)閉)
- (WKNavigation *)goBack;
//下一頁
- (WKNavigation *)goForward;
//加載
- (WKNavigation *)loadRequest:(NSURLRequest *)request;
//執(zhí)行JS方法
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^)(id, NSError *error))completionHandler;
??我們可以看到WKWebView中的屬性和方法還是很多的逗旁,上面這些方法和屬性平時用到的幾率很大。在屬性中舆瘪,我們看到有個estimatedProgress這個屬性片效,通過這個屬性我們再結(jié)合UIProgressView就能給用戶一個很好的UI展示來展示當(dāng)前頁面的加載進度。此外通過WKWebView的goBack和goForward方法我們可以控制WKWebView的加載上一頁還是下一頁英古,或者是通過stopLoading停止當(dāng)前頁面加載淀衣。還有一個evaluateJavaScript方法,我們可以用來讓webView直行JS代碼召调,這個相比于我們在創(chuàng)建WKWebView時候注入JS代碼有更好的可控性膨桥。
??下面我們說一下如何將estimatedProgress和UIProgressView相結(jié)合做出加載框的效果。我們先想一下唠叛,加載進度要實時展現(xiàn)只嚣,通過獲取這個屬性的值我們就能得到,但要實時展現(xiàn)艺沼,也許你已經(jīng)想到需要用KVO監(jiān)聽這個屬性值的變化册舞,來實時刷新UI,這是一個好的方法障般。
- (UIProgressView* )loadingProgressView {
if (!_loadingProgressView) {
_loadingProgressView = [[UIProgressView alloc]initWithFrame:CGRectMake(0, NAVI_HEIGHT, self.view.bounds.size.width, 2)];
_loadingProgressView.progressTintColor = _loadingProgressColor?_loadingProgressColor:Main_COLOR;
}
return _loadingProgressView;
}
//進度監(jiān)聽
[_wkWebView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:NULL];
監(jiān)聽變化處理
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"estimatedProgress"]) {
_loadingProgressView.progress = [change[@"new"] floatValue];
if (_loadingProgressView.progress == 1.0) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
_loadingProgressView.hidden = YES;
});
}
}
}
??當(dāng)estimatedProgress值不停變化调鲸,我們的progress也不停變化盛杰,當(dāng)progress的值到1.0的時候,說明網(wǎng)頁已經(jīng)加載完成了线得,在這我用了個延時執(zhí)行饶唤,之后讓progress隱藏,然后當(dāng)webView有了新的請求再次加載的時候再讓progress顯示贯钩。
//頁面開始加載
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation{
webView.hidden = NO;
_loadingProgressView.hidden = NO;
if ([webView.URL.scheme isEqual:@"about"]) {
webView.hidden = YES;
}
}
??當(dāng)網(wǎng)頁加載時間過久或者沒有網(wǎng)絡(luò)時募狂,給用戶提示當(dāng)前網(wǎng)絡(luò)環(huán)境不好,讓用戶選擇重新加載或者是退出網(wǎng)頁加載角雷。
添加手勢支持
添加對于多網(wǎng)頁連續(xù)加載的上下頁的水平橫掃返回上一頁下一頁的手勢祸穷,需要用到上面我們提到那個allowsBackForwardNavigationGestures這個屬性,我們需要設(shè)置其為YES勺三。這樣當(dāng)我們加載一連續(xù)的頁面時雷滚,可以通過屏幕側(cè)邊欄橫掃進行上一頁下一頁的切換。
添加下拉刷新
剛才我們提到WKWebView還有一個scrowView的屬性吗坚,在scrolView下還有一個在iOS10之后新的屬性是refreshControl祈远,這個屬性當(dāng)我們設(shè)置之后,可以支持下拉刷新操作商源,webView會重新加載车份。這個屬性是UIRefreshControl類,因此我們需要手動設(shè)置一個UIRefreshControl來添加刷新事件,讓webView重新加載牡彻。
- (UIRefreshControl* )refreshControl {
if (!_refreshControl) {
_refreshControl = [[UIRefreshControl alloc]init];
[_refreshControl addTarget:self action:@selector(webViewReload) forControlEvents:UIControlEventValueChanged];
}
return _refreshControl;
}
- (void)webViewReload {
if (_wkWebView.hidden) {
_wkWebView.hidden = NO;
}
[_wkWebView reload];
}
至此我們基本完成了一個WKWebView的封裝扫沼,對于加載一般的網(wǎng)頁,WKWebView還是很好用的庄吼。
總結(jié)
從開始著手寫WKWebView到寫完后形成此文,這期間我也參看了一些資料,不過個人感覺最行之有效的還是通過我們對于需求的分析然后查看Apple的開發(fā)文檔,最詳細(xì)也最權(quán)威,這沒什么好說的,當(dāng)然如果你英文好,自己在看文檔解讀時感受可能會更深,比硬生生的翻譯成中文更能理解其中奧義.當(dāng)然這期間寫的還是比較倉促,代碼中海油很多可優(yōu)化的余地,有更好的思路,還請大家不吝賜教.
最后附上github地址:WHWebView連接地址