惯例先看效果图(后附demo地址)
在iOS项目开发中,绝大多数功能都是我们原生开发的,但是像一些诸如用户协议说明,公司介绍,App内的活动页以及引用的其他网页都需要借助web页面来实现,有的页面简单,只需要加载一个简单的url即可,有的页面则需要用到OC和JS的交互来实现。
提到WebView,我们可能首先想起UIWebView,在iOS8.0之前,都是用此来加载网页,但是当我们打开Apple的开发文档(在此插一句题外话,当我们开发的时候肯定会遇到一些问题,就拿这个UIWebView来说,我们想知道它加载网页的方法,因此我们可能首先想到的就是去百度一下看看,有时候却发现找来找去,不同的人有不同的写法甚至是写的并不对,或者说的并不明白,这时候我建议还是查看Apple的官方文档,我们要习惯看Apple官方文档,因为不仅权威而且还原汁原味。)发现UIWebView是在UIKit下的Content Views中。打开UIWebView后,有个提示:
In apps that run in iOS 8 and later, use the
WKWeb
class instead of usingView UIWebView
. Additionally, consider setting theWKPreferences
propertyjava
toScript Enabled NO
if you render files that are not supposed to run JavaScript.
意思是说在iOS 8.0之后用WKWebView来替代UIWebView,此外WKWebView对于和JS的交互相比较UIWebView而言还需要不同的设置。苹果在此让我们用WKWebView来替代UIWebView,我们再来看下WKWebView,通过Apple开发文档发现有一个专门的WebKit,并不是在UIKit中。
UIWebView是在UIKit下
WKWebView是在WebKit下
UIWebView就这么被替代了,相信很多开发者还是很习惯UIWebView来开发的,因为使用方法相对简单,但是相较于UIWebView,WKWebView更强大,他有什么新特性呢?
1.更多的支持HTML5的特性
2.官方宣称的高达60fps的滚动刷新率以及内置手势
3.Safari相同的JavaScript引擎
4.将UIWebViewDelegate与UIWebView拆分成了14类与3个协议(官方文档说明)
5.另外用的比较多的,增加加载进度属性:estimatedProgress
哇,一看这么多优点。。。这些优点都是相对于UIWebView而言的,真是没有比较就没有伤害。对于上面官方说的WKWebView的新特性,有的开发者专门那UIWebView和WKWebView去测试了,确实发现WKWebView比UIWebView在内存上和加载速度上明显更胜一筹,在实际开开发中,我们也能明显感觉得到,加载同样一个网页,WKWebView比UIWebView相应速度明显快很多。
对于UIWebView的使用方法,在这里就不多赘述了,Apple开发文档有详细的说明也比较简单,本文的重点是在我们开发中封装一个公用的WebView,因此我们的主角还是WKWebView,当然,考虑到系统适配,我们仍然可以使用UIWebView,但是现在运行iOS8.0的设备已经很少了吧,如果要是为了考虑到自己项目的兼容性,可以做一个系统版本的适配。再次我们就以WKWebView为主。
开始前的思考
在开始之前,我们先想一下我们的Web请求一般会有怎样的应用场景
1.加载简单的一个页面 无附加操作
2.原生(OC)和web页面(JS)交互;包括JS调用OC和OC执行JS代码两种方式
其中第一点很好实现,基本不用多做处理,第二点,OC和JS交互就比较复杂了,首先我们先对WKWebView初始化:
- (WKWebView* )wkWebView {
if (!_wkWebView) {
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc]init];
config.preferences = [[WKPreferences alloc]init];
config.allowsInlineMediaPlayback = YES;
config.selectionGranularity = YES;
//自定义配置,一般用于js调用oc方法(OC拦截URL中的数据做自定义操作)
WKUserContentController *userContentController = [[WKUserContentController alloc] init];
[userContentController addScriptMessageHandler:self name:@"backPresent"];
//JS注入 向网页中添加自己的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;
//添加此属性可触发侧滑返回上一网页与下一网页操作
_wkWebView.allowsBackForwardNavigationGestures = YES;
return _wkWebView;
}
WKWebViewConfiguration
在初始化方法中有一个必传参数WKWebViewConfiguration,我们首先来看一下这个WKWebViewConfiguration是什么鬼,Apple开发文档是这么说的:
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.
说白了,通过这个类我们可以设置WKWebView的网页渲染时间,网页媒体文件处理方式以及一些WKWebView的基本设置,只能在初始化的时候设置。也就是说通过设置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.
其实就是一个遵循的协议,他的作用是能把JS的消息发送给OC
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
这个协议中有两个参数一个是WKUserContentController,一个是WKScriptMessage,WKUserContentController负责将WKScriptMessage调度出来,因此要是实现JS调用OC,就要遵循这个协议并实现这个方法。而且还要将WKUserContentController和WKScriptMessage在初始化时设置到WKWebViewConfiguration中去。
WKUserContentController
A WKUserContentController object provides a way for JavaScript to post messages and inject user scripts to a web view.
WKUserContentController有两个核心方法:
//添加JS调用OC的桥梁,这里的name就是WKScriptMessage中的name。
- (void)addScriptMessageHandler:(id)scriptMessageHandler name:(NSString *)name;
//向请求的页面中注入JS代码,如果需要在请求的页面中加入自己的JS代码,用此方法。
- (void)addUserScript:(WKUserScript *)userScript;
WKScriptMessage
WKScriptMessage就是JS通知OC时候携带的数据,其中常用的属性有
1.name
//name就是对应中的name
- (void)addScriptMessageHandler:(id)scriptMessageHandler name:(NSString *)name;
2.body 携带JS传送的数据
WKUserScript
WKUserScript主要作用就是携带正确可执行的JS代码然后注入到要请求的webView中,有一个初始化方法
- (instancetype)initWithSource:(NSString *)source injectionTime:(WKUserScriptInjectionTime)injectionTime forMainFrameOnly:(BOOL)forMainFrameOnly;
其中WKUserScriptInjectionTime是个枚举类型,以供选择合适执行JS代码。
typedef enum WKUserScriptInjectionTime : NSInteger {
WKUserScriptInjectionTimeAtDocumentStart,//开始时注入
WKUserScriptInjectionTimeAtDocumentEnd //结束时注入
} WKUserScriptInjectionTime;
熟悉了以上几个类,我们就可以初始化一个WKWebView并且配置我们用以OC和JS交互的一些配置,在此我使用了懒加载的方式:
- (WKWebView* )wkWebView {
if (!_wkWebView) {
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc]init];
config.preferences = [[WKPreferences alloc]init];
config.allowsInlineMediaPlayback = YES;
config.selectionGranularity = YES;
//自定义配置,一般用于js调用oc方法(OC拦截URL中的数据做自定义操作)
WKUserContentController *userContentController = [[WKUserContentController alloc] init];
[userContentController addScriptMessageHandler:self name:@"backPresent"];
//JS注入 向网页中添加自己的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;
//添加此属性可触发侧滑返回上一网页与下一网页操作
_wkWebView.allowsBackForwardNavigationGestures = YES;
//下拉刷新
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 10.0 && _canDownRefresh) {
_wkWebView.scrollView.refreshControl = self.refreshControl;
}
//进度监听
[_wkWebView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:NULL];
}
return _wkWebView;
}
在这段代码中,我们首先设置了WKWebView的基本配置WKWebViewConfiguration,然后再配置WKUserContentController,将我们要JS调用OC的方法实现,如果还需要向JS中注入我们添加的JS方法,则再配置WKUserScript,完成这些之后,WKWebViewConfiguration配置就基本完成,然后我们初始化WKWebView并给他一个frame。下面我们再看下WKWebView的两个代理。
在使用UIWebView的时候,我们只设置UIWebViewDelegate就好了,在这个协议中包含了很多WebView的方法,包括加载,加载完成等,而在WKWebView中,苹果将其分解为WKNavigationDelegate和WKUIDelegate两部分,其实个人感觉这样分下来更清晰,我们知道一个WebView的组件加载网页的时候,一般就两个方面:
1.WebView对于网页的渲染,这更多的是体现在UI上的提现,没有很多页面逻辑的处理。
2.WebView对于网页的加载,包括开始加载,加载完成,以及OC与JS的交互,这一层更多的是体现的是原生与Web页面的交互,包含更多的交互。
基于这两点,我们发现WKUINavigationDelegate和WKUIDelegate这两个代理很好的将UI和加载交互分开来了,让我们可以更专注UI和交互不同模块。
WKUINavigationDelegate
这个代理中包含很多方法,我们看下比较常用的。
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation;
见名知意,这个方法是当webView加载web页面的时候调用的。当开始加载页面时,我们可以在这里做想做的操作。
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation;
与上面方法相反,当页面加载完成时会调用此方法。
- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error;
加载页面出错时调动,我们可以在此做进一步操作,比如重新加载
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
这个方法决定返回方法是否允许加载,其中有一个block参数来决定我们是否允许加载,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;
//确认框
- (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;
此外还有创建新的WebView和关闭WebView的方法,还有关于ForceTouch的支持方法,在文档中都有详细的说明。
了解这些以后,我们就基本可以创建出一个WKWebView,而且能实现与OC与JS的一些交互,通过设置他的代理方法来实现加载进度控制等,不过在WebView中,我们还会有点击下一页或者返回上一页,或者对于加载一个进度的展示等需求,这些我们也要考虑在内。
我们来看下WKWebView中有哪些属性和方法。
Property
title 标题
URL 当前url
scrollView
estimatedProgress 加载进度 浮点型
allowsBackForwardNavigationGestures 手势支持
Method
// HTML加载方法
- (WKNavigation *)loadHTMLString:(NSString *)string baseURL:(NSURL *)baseURL;
//重新加载
- (WKNavigation *)reload;
//停止加载
- (void)stopLoading;
//上一页(不是关闭)
- (WKNavigation *)goBack;
//下一页
- (WKNavigation *)goForward;
//加载
- (WKNavigation *)loadRequest:(NSURLRequest *)request;
//执行JS方法
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^)(id, NSError *error))completionHandler;
我们可以看到WKWebView中的属性和方法还是很多的,上面这些方法和属性平时用到的几率很大。在属性中,我们看到有个estimatedProgress这个属性,通过这个属性我们再结合UIProgressView就能给用户一个很好的UI展示来展示当前页面的加载进度。此外通过WKWebView的goBack和goForward方法我们可以控制WKWebView的加载上一页还是下一页,或者是通过stopLoading停止当前页面加载。还有一个evaluateJavaScript方法,我们可以用来让webView直行JS代码,这个相比于我们在创建WKWebView时候注入JS代码有更好的可控性。
下面我们说一下如何将estimatedProgress和UIProgressView相结合做出加载框的效果。我们先想一下,加载进度要实时展现,通过获取这个属性的值我们就能得到,但要实时展现,也许你已经想到需要用KVO监听这个属性值的变化,来实时刷新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;
}
//进度监听
[_wkWebView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:NULL];
监听变化处理
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)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;
});
}
}
}
当estimatedProgress值不停变化,我们的progress也不停变化,当progress的值到1.0的时候,说明网页已经加载完成了,在这我用了个延时执行,之后让progress隐藏,然后当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;
}
}
当网页加载时间过久或者没有网络时,给用户提示当前网络环境不好,让用户选择重新加载或者是退出网页加载。
添加手势支持
添加对于多网页连续加载的上下页的水平横扫返回上一页下一页的手势,需要用到上面我们提到那个allowsBackForwardNavigationGestures这个属性,我们需要设置其为YES。这样当我们加载一连续的页面时,可以通过屏幕侧边栏横扫进行上一页下一页的切换。
添加下拉刷新
刚才我们提到WKWebView还有一个scrowView的属性,在scrolView下还有一个在iOS10之后新的属性是refreshControl,这个属性当我们设置之后,可以支持下拉刷新操作,webView会重新加载。这个属性是UIRefreshControl类,因此我们需要手动设置一个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的封装,对于加载一般的网页,WKWebView还是很好用的。
总结
从开始着手写WKWebView到写完后形成此文,这期间我也参看了一些资料,不过个人感觉最行之有效的还是通过我们对于需求的分析然后查看Apple的开发文档,最详细也最权威,这没什么好说的,当然如果你英文好,自己在看文档解读时感受可能会更深,比硬生生的翻译成中文更能理解其中奥义.当然这期间写的还是比较仓促,代码中海油很多可优化的余地,有更好的思路,还请大家不吝赐教.
最后附上github地址:WHWebView连接地址