iOS 学习之 WKWebView 使用

WebKit 框架概览

WKWebView 是 iOS8 中引入的新组建,苹果将 UIWebViewDelegate 与 UIWebView 重构成了14个类和3个协议并引入了不少新的功能和接口,它代替了 UIKit 中的 UIWebView 和 Appkit 中的 WebView,提供了统一的跨双平台 API。(iOS 和 macOS)

并且最吸引人的在于很多方法和 UIWebView 类似,从 UIWebView 转到 WKWebView 可以很迅速,在不考虑适配 iOS8 系统以下的情况下,WKWebView 是一个很不错的选择。它的新特性包括:

  1. 在性能、稳定性、功能方面有很大的提升,最能直观的体现就是加载网页时占用的内存,在模拟器加载百度时,WKWebView 占用23M,而 UIWebView 占用85M;
  2. 和 Safari 相同的 Javascript 引擎,允许 Javascript 的 Nitro 库加载并使用;(UIWebView 中限制)
  3. 支持了更多的 HTML5 特征;
  4. 60fps刷新率,内置手势。

如上图所示,WebKit 框架中最核心的类应该属于 WKWebView 了,这个类专门用来渲染网页视图,其他类和协议都将基于它和服务于它。

  • WKWebView:网页的渲染与展示,通过 WKWebViewConfiguration 可以进行自定义配置。
  • WKWebViewConfiguration:这个类专门用来配置 WKWebView。
  • WKPreference:这个类用来进行相关 webView 设置。
  • WKProcessPool:这个类用来配置进程池,与网页视图的资源共享有关。
  • WKUserContentController:这个类主要用来做 native 与 JavaScript 的交互管理。
  • WKUserScript:用于进行 JavaScript 注入。
  • WKScriptMessageHandler:这个类专门用来处理 JavaScript 调用 native 的方法。
  • WKNavigationDelegate:网页跳转间的导航管理协议,这个协议可以监听网页的活动。
  • WKNavigationAction:网页某个活动的示例化对象。
  • WKUIDelegate:用于交互处理 JavaScript 中的一些弹出框。
  • WKBackForwardList:堆栈管理的网页列表。
  • WKBackForwardListItem:每个网页节点对象。

WKWebKit 的属性及方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/// webView 的自定义配置
@property (nonatomic, readonly, copy) WKWebViewConfiguration *configuration;

/// 导航代理
@property (nullable, nonatomic, weak) id <WKNavigationDelegate> navigationDelegate;

/// UI 代理
@property (nullable, nonatomic, weak) id <WKUIDelegate> UIDelegate;

/// 访问过网页历史列表
@property (nonatomic, readonly, strong) WKBackForwardList *backForwardList;

/// 自定义初始化 webView
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER;

- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;

/// url 加载 webView 视图
- (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;

/// 文件加载 webView 视图
- (nullable WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL API_AVAILABLE(macosx(10.11), ios(9.0));

/// HTMLString 字符串加载 webView 视图
- (nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;

/// NSData 数据加载 webView 视图
- (nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString *)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL API_AVAILABLE(macosx(10.11), ios(9.0));

/// 返回上一个网页节点
- (nullable WKNavigation *)goToBackForwardListItem:(WKBackForwardListItem *)item;

/// 网页的标题
@property (nullable, nonatomic, readonly, copy) NSString *title;

/// 网页的 URL 地址
@property (nullable, nonatomic, readonly, copy) NSURL *URL;

/// 网页是否正在加载
@property (nonatomic, readonly, getter=isLoading) BOOL loading;

/// 加载的进度 范围为[0, 1]
@property (nonatomic, readonly) double estimatedProgress;

/// 网页链接是否安全
@property (nonatomic, readonly) BOOL hasOnlySecureContent;

/// 证书服务
@property (nonatomic, readonly, nullable) SecTrustRef serverTrust API_AVAILABLE(macosx(10.12), ios(10.0));

/// 是否可以返回
@property (nonatomic, readonly) BOOL canGoBack;

/// 是否可以前进
@property (nonatomic, readonly) BOOL canGoForward;

/// 返回到上一个网页
- (nullable WKNavigation *)goBack;

/// 前进到下一个网页
- (nullable WKNavigation *)goForward;

/// 重新加载
- (nullable WKNavigation *)reload;

/// 忽略缓存 重新加载
- (nullable WKNavigation *)reloadFromOrigin;

/// 停止加载
- (void)stopLoading;

/// 执行 JavaScript
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;

/// 是否允许左右滑动,返回-前进操作 默认是 NO
@property (nonatomic) BOOL allowsBackForwardNavigationGestures;

/// 自定义代理字符串
@property (nullable, nonatomic, copy) NSString *customUserAgent API_AVAILABLE(macosx(10.11), ios(9.0));

/// 在 iOS 上默认为 NO,标识不允许链接预览
@property (nonatomic) BOOL allowsLinkPreview API_AVAILABLE(macosx(10.11), ios(9.0));

/// 滚动视图
@property (nonatomic, readonly, strong) UIScrollView *scrollView;

/// 是否支持放大手势,默认为 NO
@property (nonatomic) BOOL allowsMagnification;

/// 放大因子,默认为1
@property (nonatomic) CGFloat magnification;

/// 据设置的缩放因子来缩放页面,并居中显示结果在指定的点
- (void)setMagnification:(CGFloat)magnification centeredAtPoint:(CGPoint)point;

用来追踪加载过程(页面开始加载、加载完成、加载失败)的方法:

1
2
3
4
5
6
7
8
// 页面开始加载时调用
- (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;

页面跳转的代理方法:

1
2
3
4
5
6
// 接收到服务器跳转请求之后调用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation;
// 在收到响应后,决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
// 在发送请求之前,决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;

WKNavigationDelegate 的调用顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//  在发送请求之前,决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
    
    decisionHandler(WKNavigationActionPolicyAllow);
}

// 页面开始加载时调用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation{
    
}

// 在发送请求之前,决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{

    decisionHandler(WKNavigationActionPolicyAllow);
}

// 接收到服务器跳转请求之后调用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation{

}

// 权限认证的时候调用
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *__nullable credential))completionHandler{
    
    completionHandler(NSURLSessionAuthChallengePerformDefaultHandling ,nil);
}


/**** 以下三个是连续调用 ****/

// 在收到响应后,决定是否跳转和发送请求之前那个允许配套使用
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{

    decisionHandler(WKNavigationResponsePolicyAllow);
}

// 当内容开始返回时调用
- (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation{
    
}

// 页面加载完成之后调用
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation{
  
}

WKWebView 调用 JavaScript

前提:必须导入 #import <WebKit/WebKit.h> ,遵循 WKNavigationDelegate 代理方法

在 WKWebView 中调用 JavaScript 有两个方案:

1、在初始化 WebView 的时候加在配置文件中引入 JavaScript 的方法

2、在恰当的时机执行 evaluateJavaScript:completionHandler: 方法

方法一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
举个例子,我们在这里实现 WKWebView 图片自适应屏幕宽度,设置网页最小字体大小为14.5

// JavaScript 的代码
NSString *jScript = @"var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta); var imgs = document.getElementsByTagName('img');for (var i in imgs){imgs[i].style.maxWidth='100%';imgs[i].style.height='auto';} ";

// 注入 JavaScript
WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jScript injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
WKUserContentController *wkUController = [[WKUserContentController alloc] init];
[wkUController addUserScript:wkUScript];

WKWebViewConfiguration *wkWebConfig = [[WKWebViewConfiguration alloc] init];
wkWebConfig.userContentController = wkUController;
wkWebConfig.allowsInlineMediaPlayback = YES;
if ([UIDevice currentDevice].systemVersion >= 9.0) {
wkWebConfig.allowsPictureInPictureMediaPlayback = YES;
}

// 设置网页最小字体大小为14.5
WKPreferences *preference = [[WKPreferences alloc]init];
preference.minimumFontSize = 14.5;
wkWebConfig.preferences = preference;

_webView = [[WKWebView alloc]initWithFrame:CGRectNull configuration:wkWebConfig];
[_webView setFrame:[UIScreen mainScreen].bounds];
_webView.scrollView.showsVerticalScrollIndicator = NO;
_webView.scrollView.showsHorizontalScrollIndicator = NO;
_webView.navigationDelegate = self;
[self.view addSubview:_webView];

方法二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation
{
// 禁止 WKWebView 的捏合手势,双击放大缩小等操作
NSString *javascript = @"var meta = document.createElement('meta');meta.setAttribute('name', 'viewport');meta.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no');document.getElementsByTagName('head')[0].appendChild(meta);";
[webView evaluateJavaScript:javascript completionHandler:nil];
}

- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
// 修改 WKWebView 中的所有字体颜色为红色
[webView evaluateJavaScript:@"document.getElementsByTagName('body')[0].style.webkitTextFillColor= '#ff0000'" completionHandler:nil];

// 修改网页中 table 标签,把表格的线宽改为5px,线的颜色改为红色
NSString *jScript = @"function compatTable(){var tableElements=document.getElementsByTagName(\"table\");for(var i=0;i<tableElements.length;i++){var tableElement=tableElements[i];tableElement.cellspacing=\"\";tableElement.cellpadding=\"\";tableElement.border=\"\";tableElement.setAttribute(\"style\",\"border-collapse:collapse; display:table;\")}var tdElements=document.getElementsByTagName(\"td\");for(var i=0;i<tdElements.length;i++){var tdElement=tdElements[i];tdElement.valign=\"\";tdElement.width=\"\";tdElement.setAttribute(\"style\",\"border:5px solid red;\");tdElement.setAttribute(\"contenteditable\",\"false\")}};compatTable();";
[webView evaluateJavaScript:jScript completionHandler:nil];
}

当然了,上面举了三个案例,细心的读者会有发现 completionHandler: 这个回调方法,似乎都返回的 nil,没有做任何回调。那么下面我在列举一个比较常见的场景 - 获取当前点中的图像链接以及网页中的所有链接。

遵循 WKNavigationDelegate 代理方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// self.imgUrls 是一个 NSArray 的属性
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
NSString *getImgUrlsJS = @"\
function getImgUrls() {\
var imgs = document.getElementsByTagName('img');\
var urls = [];\
for (var i = 0; i < imgs.length; i++) {\
var img = imgs[i];\
urls[i] = img.src;\
}\
return urls;\
}";

[webView evaluateJavaScript:getImgUrlsJS completionHandler:nil];
[webView evaluateJavaScript:@"getImgUrls()" completionHandler:^(id _Nullable obj, NSError * _Nullable error) {
NSLog(@"网页中的所有图像链接:%@", obj);
self.imgUrls = obj;
}];

NSString *imgClickJS = @"function imgClickAction(){var imgs=document.getElementsByTagName('img');var length=imgs.length;for(var i=0; i < length;i++){img=imgs[i];if(\"ad\" ==img.getAttribute(\"flag\")){var parent = this.parentNode;if(parent.nodeName.toLowerCase() != \"a\")return;}img.onclick=function(){window.location.href='image-preview:'+this.src}}}";
[webView evaluateJavaScript:imgClickJS completionHandler:nil];
[webView evaluateJavaScript:@"imgClickAction()" completionHandler:nil];
}

// self.imgUrls 是一个 NSArray 的属性
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
NSString *url = navigationAction.request.URL.absoluteString;
if ([url hasPrefix:@"image-preview:"]) {
NSString *imgUrl = [url substringFromIndex:14];
NSInteger index = [self.imgUrls indexOfObject:imgUrl];
NSLog(@"当前点中的图像链接是:%@",self.imgUrls[index]);
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
decisionHandler(WKNavigationActionPolicyAllow);
}

WKUIDelegate 的回调方法

假如网页中含有一些 JavaScript 的操作执行,这个时候就触发它的另一个代理 WKUIDelegate 的回调方法。

1
2
3
4
5
6
7
8
// 在 JS 端调用 alert 函数时,会触发此代理方法。
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler;

// JS 端调用 confirm 函数时,会触发此代理方法。
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler;

// JS 端调用 prompt 函数时,会触发此代理方法。
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler;

特别注意:以上方法需要在方法的结束写上回调。

1
completionHandler();

JavaScript 中调用 Objective-C 的方法

当 Web 端想传一些数据给 iOS,那它们会调用 messageHandlers: postMessage: 方法来发送给我们。
此时我们所需要做的就是遵循 WKScriptMessageHandler 的代理方法,name 和上方 JavaScript 中的方法名相对应。

1
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;

具体操作如下:

1
2
3
4
5
6
7
[[_webView configuration].userContentController addScriptMessageHandler:self name:@"方法名"];

// WKScriptMessageHandler协议方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {

}