iOS UIWebView 与 WKWebView 用法

一、UIWebView

UIWebView基本用法

UIWebView可以加载本地或远程HTML页面。使用UIWebView的loadRequest:即可完成加载工作,十分简单。但一般我们加载本地HTML页面时,会使用UIWebView的loadHTMLString:方法,这么做有两点好处:一是安全,二是避免中文乱码:

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
#import "ViewController.h"

@interface ViewController ()<UIWebViewDelegate>{
UIWebView *myWebView;
}

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];

myWebView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height)];
myWebView.delegate = self;
[self.view addSubview:myWebView];

//[self loadLocalHtml];
[self loadRemoteHtml];
}

//-----------加载本地html文件--------------//
- (void)loadLocalHtml {
//To help you avoid being vulnerable to security attacks, be sure to use this method to load local HTML files; don’t use loadRequest:.
//[myWebView loadRequest:[[NSURLRequest alloc] initWithURL:[[NSURL alloc] initFileURLWithPath:htmlPath]]];
NSString *htmlPath = [[NSBundle mainBundle] pathForResource:@"hello" ofType:@"html"];
NSString *htmlString = [[NSString alloc] initWithContentsOfFile:htmlPath encoding:NSUTF8StringEncoding error:nil];
[myWebView loadHTMLString: htmlString baseURL: nil];
}

//-----------加载远程html文件--------------//
- (void)loadRemoteHtml {
[myWebView loadRequest:[[NSURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://www.baidu.com"]]];
}

#pragma mark --webViewDelegate
-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSLog(@"网页加载之前会调用webView:shouldStartLoadWithRequest:navigationType:方法");

//retrun YES 表示正常加载网页 返回NO 将停止网页加载
return YES;
}

-(void)webViewDidStartLoad:(UIWebView *)webView
{
NSLog(@"开始加载网页时调用webViewDidStartLoad:方法");
}

-(void)webViewDidFinishLoad:(UIWebView *)webView
{
NSLog(@"网页加载完成调用webViewDidFinishLoad:方法");
}

-(void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
NSLog(@"网页加载失败会调用webView:didFailLoadWithError:方法");
}

@end

hello.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE HTML>
<html>
<body>
<H2 id = "myName">Hi,I`m Liyanliang</H2>
<canvas id="myCanvas" width="200" height="100" style="border:1px solid #c3c3c3;">
Your browser does not support the canvas element.
</canvas>
<br/>
<a href="https://www.tmall.com">前往天猫商城(OC中会强行重定向到京东)</a>
<br/>
<input type="button" value="JS调用iOS方法-UIWebView处理" onclick="callOCMethord()" />
<input type="button" value="JS调用iOS方法-WKWebView处理" onclick="callOCMethord2()" />
<script src="myjs.js"></script>
</body>
</html>

myjs.js

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
function drawLinearGradient() {
var c=document.getElementById("myCanvas");
var cxt=c.getContext("2d");
var grd=cxt.createLinearGradient(0,0,175,50);
grd.addColorStop(0,"#FF0000");
grd.addColorStop(1,"#00FF00");
cxt.fillStyle=grd;
cxt.fillRect(0,0,175,50);
}

function sendCommand(cmd,param){
var url="jsCalledOCCommand:"+cmd+":"+param;
document.location = url;
}

function callOCMethord(){
sendCommand("Alert","这是JS传递的内容");
}

function callOCMethord2(){
//window.webkit.messageHandlers.<name>.postMessage();
window.webkit.messageHandlers.TestJSMessage.postMessage("这是JS传递的内容");
}

//alert("JS Alert");

如果你使用的iOS版本在9.0及9.0之上,运行上面代码,控制台会输出警告:

1
App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app’s Info.plist file.

解决办法请参考我的另外一篇文章:iOS网络开发中的) 2.1 章节。

解决UIWebView Delegate方法多次调用问题

我们看一下UIWebView的几个Delegate方法。如果您运行上述示例程序,会发现这几个Delegate方法会执行多次,这是因为网页内有异步请求或者重定向时,就会多次调用这几个Delegate方法,解决方法是使用webView.isLoading属性(详情请参考stackoverflow):

1
2
3
4
5
6
7
-(void)webViewDidFinishLoad:(UIWebView *)webView
{
if (webView.isLoading) {
return;
}
NSLog(@"网页加载完成调用webViewDidFinishLoad:方法");
}

UIWebView调用JS方法

使用UIWebView的stringByEvaluatingJavaScriptFromString:即可执行JS方法。需要注意的是,对于本地JS文件,我们也需要先执行stringByEvaluatingJavaScriptFromString:,之后才能调用本地JS文件中的具体JS方法。

即便在您的HTML文件中引用了本地js文件,如:<script src="myjs.js"></script>,如果要在OC中调用该JS文件中的相关方案,依然需要使用stringByEvaluatingJavaScriptFromString:把整个JS文件包含进来,才能调用本地JS文件中的具体JS方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-(void)webViewDidFinishLoad:(UIWebView *)webView
{
NSLog(@"网页加载完成调用webViewDidFinishLoad:方法");

//NSLog(@"webView.request.URL.absoluteString:%@,\n document.location.href:%@", webView.request.URL.absoluteString,[webView stringByEvaluatingJavaScriptFromString:@"document.location.href"]);

if (webView.isLoading) {
return;
}
//执行js
//NSString *jsString = @"alert('百度首页加载完毕')";
//[myWebView stringByEvaluatingJavaScriptFromString:jsString];
//执行本地JS文件中的方法,需要使用stringByEvaluatingJavaScriptFromString方法把整个JS文件包含进来。
NSString *jsPath = [[NSBundle mainBundle] pathForResource:@"myjs" ofType:@"js"];
NSString *jsString = [[NSString alloc] initWithContentsOfFile:jsPath encoding:NSUTF8StringEncoding error:nil];
[myWebView stringByEvaluatingJavaScriptFromString:jsString];
[myWebView stringByEvaluatingJavaScriptFromString:@"drawLinearGradient();"];

}

UIWebView管理页面跳转

我们在开发中会遇到这样的问题:比如我是京东员工(不要猜测,我本人真不是京东员工),上级告诉我,在我们的APP中,任何跳转到天猫的行为都要被阻止,并强行跳转到京东首页。你会怎么做?

我们可以在webView:shouldStartLoadWithRequest:navigationType:这个代理方法中管理页面跳转。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
//NSLog(@"网页加载之前会调用webView:shouldStartLoadWithRequest:navigationType:方法");
//retrun YES 表示正常加载网页 返回NO 将停止网页加载

//---------------------该代理方法可以实现 1:页面重定向------------------------//
if (!webView.isLoading) {

//网上教程有使用js的document.location.href获取地址,在此不能这样使用。
//NSString *willToURL = [webView stringByEvaluatingJavaScriptFromString:@"document.location.href"];
NSString *willToURL = [[request URL] absoluteString];
//NSLog(@"willToURL-------%@",willToURL);
if ([willToURL containsString:@"tmall.com"]) {
[myWebView loadRequest:[[NSURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://www.jd.com"]]];
return NO;
}
return YES;
}
else {
return NO;
}

}

UIWebView实现JS调用OC方法

同样也是在webView:shouldStartLoadWithRequest:navigationType:这个代理方法中实现。其思路是:

1、js中定义一个特殊的url,并控制document.location跳转到该url。

1
2
3
4
function sendCommand(cmd,param){
var url="jsCalledOCCommand:"+cmd+":"+param;
document.location = url;
}

2、OC代码中,在webView:shouldStartLoadWithRequest:navigationType:这个代理方法中,监控页面跳转,如果发现是我们规定好的那个特殊的url,则调用相应OC方法。

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

-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
//NSLog(@"网页加载之前会调用webView:shouldStartLoadWithRequest:navigationType:方法");
//retrun YES 表示正常加载网页 返回NO 将停止网页加载

//------------------该代理方法可以实现 2:通过js调用OC方法---------------------//
if (!webView.isLoading) {
NSString *willToURL = [[request URL] absoluteString];
//testapp:alert:%E4%BD%A0%E5%A5%BD%E5%90%97%EF%BC%9F
NSArray *components = [willToURL componentsSeparatedByString:@":"];

//!!!!!!!!!!!!!!!!!!!!注意注意注意!!!!!!!!!!!!!!!!!!!!
//注意:jsCalledOCCommand 会自动转换为小写
if ([components count] >= 3
&& [[components objectAtIndex:0] isEqualToString:@"jscalledoccommand"]
&& [[components objectAtIndex:1] isEqualToString:@"Alert"]) {

NSString *message = [[components objectAtIndex:2] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
//iOS9之后该方法过时,建议用stringByAddingPercentEncodingWithAllowedCharacters
//但我实在没实验出该怎么用,麻烦哪位读者能告知一下
//NSString *message = [components objectAtIndex:2];
//message = [message stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet alphanumericCharacterSet]];
//message = [message stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];

[self showAlertMessageWithTitle:@"来自JS的呼唤" message:message];

return NO;
}
return YES;
}
else {
return NO;
}

}

- (void)showAlertMessageWithTitle:(NSString *)title message:(NSString *)message {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
//UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleDestructive handler:nil];
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"用Safari打开百度" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"http://www.baidu.com"]];
}];

[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField){
textField.placeholder = @"登录";
}];
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = @"密码";
textField.secureTextEntry = YES;
}];
[alertController addAction:cancelAction];
[alertController addAction:okAction];
[self presentViewController:alertController animated:YES completion:nil];
}

使用Safari打开URL

上面代码中我们也看到了,我们想要使用默认浏览器来打开web,只用调用UIApplication单例的openURL方法即可。

1
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"http://www.baidu.com"]];

UIWebView 加载其它类型的文件

UIWebView打开本地pdf、word文件依靠的并不是UIWebView自身解析,而是依靠MIME Type识别文件类型并调用对应应用打开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//    //加载 PDF 内容
// NSURL *pdfFileURL = [[NSBundle mainBundle] URLForResource:@"OReilly.Learning.AngularJS.2015.3" withExtension:@"pdf"];
// NSData *pdfFileData = [NSData dataWithContentsOfURL:pdfFileURL];
// [myWebView loadData:pdfFileData MIMEType:@"application/pdf" textEncodingName:@"utf-8" baseURL:pdfFileURL];

//加载 PDF 内容 , 用loadRequest方法更简单
NSURL *pdfFileURL = [[NSBundle mainBundle] URLForResource:@"OReilly.Learning.AngularJS.2015.3" withExtension:@"pdf"];
[myWebView loadRequest:[NSURLRequest requestWithURL:pdfFileURL]];

//加载 xlsx 内容--不行
// NSURL *fileURL = [[NSBundle mainBundle] URLForResource:@"xlsx test" withExtension:@"xls"];
// NSData *fileData = [NSData dataWithContentsOfURL:fileURL];
// [myWebView loadData:fileData MIMEType:@"application/vnd.ms-excel" textEncodingName:@"utf-8" baseURL:fileURL];

//加载 txt 内容
// NSURL *fileURL = [[NSBundle mainBundle] URLForResource:@"txtTest" withExtension:@"txt"];
// NSData *fileData = [NSData dataWithContentsOfURL:fileURL];
// [myWebView loadData:fileData MIMEType:@"text/plain" textEncodingName:@"utf-8" baseURL:fileURL];

避免 UIWebView 内存泄露

UIWebView Class Reference中有这么一段:

IMPORTANT

Before releasing an instance of UIWebView for which you have set a delegate, you must first set its delegate property to nil. This can be done, for example, in your dealloc method.

所以,我们需要这么做来防止内存泄露:

1
2
3
4
- (void)dealloc {
myWebView.delegate = nil;
myWebView = nil;
}

二、WKWebView

WKWebView新特性

ios8之后,推荐用WKWebView来替代UIWebView。WKWebView相对于UIWebView内存消耗相对减少,所提供的接口也丰富了许多。大家可以先移步
WKWebView - NSHipster看看介绍。

曾经的UIWebView,在iOS上,使用的是UIKit.framework的UIWebView;在OS X上,使用的是WebKit.framework的WebView。

iOS 8 的WKWebView:

  • 和OS X使用统一的framework,意味着可移植性提高了。除此之外
  • 更好的性能,如对网页滑动的响应。
  • 更好的JavaScript引擎。
  • 内置前进后退手势。
  • 更有效的JS和App的交互。
  • 最重头的,新的多进程模型。

WebKit API

1、基本属性

网页加载进度(estimatedProgress加载进度条,在IOS8之前我们是通过一个假的进度条来实现)、网页标题,这些网页的最最基本的属性,终于齐了、backForwardList表示historyList、WKWebViewConfiguration *configuration;初始化webview的配置。

2、前进后退手势

在UIWebView这个功能十分复杂,而WKWebView内置

3、WKPreferences

对应WebView的WebViewPreference,相比UIWebView,增加了禁用JavaScript功能,但没有无图模式,差评。

4、WKUserContentController

JS通讯相关,App注入JS时,可以指定时机(加载开始或加载结束)和范围(MainFrame或所有Frame);另外内置JS Bridge。

5、WKProcessPool

和多进程模型相关,目前功能未知。

6、WKBackForwardList

前进后退列表。

7、WKNavigationDelegate

类似于WebView里的WebFrameLoadDelegate,功能稍稍阉割了下,但基本能用。

8、WKUIDelegate

UI相关的回调。如新窗口打开、页面alert弹框等的处理回调。

9、增加初始化方法

1
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration

10、跳到历史的某个页面

1
-(WKNavigation *)goToBackForwardListItem:(WKBackForwardListItem *)item;

11、相同的属性和方法

goBack、goForward、canGoBack、canGoForward、stopLoading、loadRequest、scrollView

12、被删去的属性和方法

在跟js交互时,我们使用这个API,目前WKWebView完档没有给出实现类似功能的API

1
- (NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;

无法设置缓存:在UIWebView,使用NSURLCache缓存,通过setSharedURLCache可以设置成我们自己的缓存,但WKWebView不支持NSURLCache

13、delegate方法的不同

UIWebView支持的代理是UIWebViewDelegate,WKWebView支持的代理是WKNavigationDelegate和WKUIDelegate

WKNavigationDelegate主要实现了涉及到导航跳转方面的回调方法

WKUIDelegate主要实现了涉及到界面显示的回调方法:如WKWebView的改变和js相关内容。

具体来说WKNavigationDelegate除了有开始加载、加载成功、加载失败的API外,还具有额外的三个代理方法:

1
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation

这个代理是服务器redirect时调用

1
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler

这个代理方法表示当客户端收到服务器的响应头,根据response相关信息,可以决定这次跳转是否可以继续进行。

1
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler

根据webView、navigationAction相关信息决定这次跳转是否可以继续进行,这些信息包含HTTP发送请求,如头部包含User-Agent,Accept。

更多细节,可以查看WebKit Objective-C Framework Reference

WKWebView加载远程HTML页面及KVO检测加载进度

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
95
96
97
98
99
#import "ViewController2.h"
#import <WebKit/WebKit.h>

@interface ViewController2 ()<WKNavigationDelegate>

@property (nonatomic,strong)WKWebView *webView;

@end

@implementation ViewController2

- (void)viewDidLoad {
[super viewDidLoad];

self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:self.webView];
self.webView.navigationDelegate = self;

//[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://aliang9585.github.io/"]]];
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.jd.com"]]];
self.webView.allowsBackForwardNavigationGestures = YES;//打开左划回退功能
//self.webView.allowsLinkPreview = YES;
}


- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];

//Add KVO
[self.webView addObserver:self forKeyPath:@"loading" options:NSKeyValueObservingOptionNew context:nil];
[self.webView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:nil];
[self.webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil];

}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:YES];
//Remove KVO
[self.webView removeObserver:self forKeyPath:@"loading" context:nil];
[self.webView removeObserver:self forKeyPath:@"title" context:nil];
[self.webView removeObserver:self forKeyPath:@"estimatedProgress" context:nil];

}


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"loading"]) {
NSLog(@"loading:%d",self.webView.loading);
}else if ([keyPath isEqualToString:@"title"]) {
NSLog(@"title:%@",self.webView.title);
}else if ([keyPath isEqualToString:@"estimatedProgress"]) {
NSLog(@"estimatedProgress:%f",self.webView.estimatedProgress);
}
}

#pragma mark - WKNavigationDelegate
//=====WKNavigationDelegate主要实现了涉及到导航跳转方面的回调方法=====//
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {

NSLog(@"1?、decidePolicyForNavigationAction-类似UIWebView的webView:shouldStartLoadWithRequest:navigationType:,根据webView、navigationAction相关信息决定这次跳转是否可以继续进行,这些信息包含HTTP发送请求,如头部包含User-Agent,Accept");
decisionHandler(WKNavigationActionPolicyAllow);

}

- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
NSLog(@"2、didStartProvisionalNavigation-类似UIWebView的webViewDidStartLoad:,页面开始加载时调用");
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
}

- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation {

NSLog(@"3?、didReceiveServerRedirectForProvisionalNavigation-服务器redirect时调用");
}


- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
NSLog(@"4、decidePolicyForNavigationResponse-当客户端收到服务器的响应头,根据response相关信息,可以决定这次跳转是否可以继续进行");
decisionHandler(WKNavigationResponsePolicyAllow);
}


- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {

NSLog(@"5、didCommitNavigation-内容开始返回时调用");
}

- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
// if (webView.isLoading) {
// return;
// }

NSLog(@"6、didFinishNavigation-类似UIWebView的webViewDidFinishLoad:,页面加载完成后调用");

}
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error {

NSLog(@"%s-类似UIWebView的webView:didFailLoadWithError:,页面加载失败时调用",__FUNCTION__);
}

@end

WKWebView 与 Javascript 交互

先贴上源码(html和js源码见上文UIWebView部分)

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
#import "ViewController3.h"
#import <WebKit/WebKit.h>

//交互用到的三大代理:
//WKNavigationDelegate,与页面导航加载相关
//WKUIDelegate,与JS交互时的ui展示相关,比较JS的alert、confirm、prompt
//WKScriptMessageHandler,与js交互相关,通常是ios端注入名称,js端通过window.webkit.messageHandlers.{NAME}.postMessage()来发消息到ios端
@interface ViewController3 ()<WKNavigationDelegate,WKUIDelegate,WKScriptMessageHandler>

@property (nonatomic,strong) WKWebView *webView;
@property (nonatomic,strong) WKWebViewConfiguration *configuretion;

@end

@implementation ViewController3

- (void)viewDidLoad {
[super viewDidLoad];
//--------------------WKPreferences------------------------
WKPreferences *preferences = [[WKPreferences alloc] init];
preferences.minimumFontSize = 10;
preferences.javaScriptEnabled = true;
//默认是不能通过JS自动打开窗口的,必须通过用户交互才能打开
preferences.javaScriptCanOpenWindowsAutomatically = false;

//--------------------WKUserContentController------------------------
WKUserContentController *userContentController = [[WKUserContentController alloc] init];

//--------------------WKWebViewConfiguration------------------------
self.configuretion = [[WKWebViewConfiguration alloc] init];
self.configuretion.preferences = preferences;
//通过js与webview内容交互配置
self.configuretion.userContentController = userContentController;

//--------------------WKWebView------------------------
//self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds];
self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:self.configuretion];
[self.view addSubview:self.webView];
self.webView.navigationDelegate = self;
self.webView.UIDelegate = self;
self.webView.allowsBackForwardNavigationGestures = YES;//打开左划回退功能

[self loadLocalHtml];
[self testAddScriptMessageName];//测试JS调OC
[self testAddUserScript];//测试使用用户脚本来注入 JavaScript
}

/**
* 加载本地html文件
*/

- (void)loadLocalHtml {
NSString *htmlPath = [[NSBundle mainBundle] pathForResource:@"hello" ofType:@"html"];
NSURL *htmlURL = [[NSURL alloc] initFileURLWithPath:htmlPath];

NSString *htmlString = [[NSString alloc] initWithContentsOfFile:htmlPath encoding:NSUTF8StringEncoding error:nil];

//方法1、loadRequest方法加载本地html文件会产生乱码
//iOS (8.0 and later)
[self.webView loadRequest:[[NSURLRequest alloc] initWithURL:htmlURL]];

//方法2、loadFileURL方法通常用于加载服务器的HTML页面或者JS
//iOS (9.0 and later)
//[self.webView loadFileURL:htmlURL allowingReadAccessToURL:nil];

//方法3、loadHTMLString方法通常用于加载本地HTML或者JS
[self.webView loadHTMLString: htmlString baseURL: nil];
}


/**
* 添加一个名称,来匹配js的回调。
* js端需要固定用window.webkit.messageHandlers.<name>.postMessage();来想OC发送消息
* OC端用WKScriptMessageHandler protocal来接收消息
*/

- (void)testAddScriptMessageName {
// 添加一个名称,就可以在JS通过这个名称发送消息:
// window.webkit.messageHandlers.AppModel.postMessage({body: 'xxx'})

[self.configuretion.userContentController addScriptMessageHandler:self name:@"TestJSMessage"];
}


/*
*使用用户脚本来注入 JavaScript
*/

//WKUserScript 对象可以以 JavaScript 源码形式初始化,初始化时还可以传入是在加载之前还是结束时注入,以及脚本影响的是这个布局还是仅主要布局。于是用户脚本被加入到 WKUserContentController 中,并且以 WKWebViewConfiguration 属性传入到 WKWebView 的初始化过程中。

//这个样例可以简单扩展为更为高级的页面修改方法,例如去除广告、隐藏评论等
- (void)testAddUserScript {
//WKUserScript *userScript = [[WKUserScript alloc] initWithSource:@"function showAlert() { alert('使用用户脚本来注入 JavaScript'); }" injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];

//1、直接注入js执行方法
// WKUserScript *userScript1 = [[WKUserScript alloc] initWithSource:@" document.getElementById('myName').innerHTML='LYL\\'s Blog'; " injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
// [self.configuretion.userContentController addUserScript:userScript1];

//2、注入js方法,和执行方法。
WKUserScript *userScript2 = [[WKUserScript alloc] initWithSource:@"function changeMyName() { document.getElementById('myName').innerHTML='LYL\\'s Blog'; }" injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
WKUserScript *userScript2_1 = [[WKUserScript alloc] initWithSource:@" changeMyName();" injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
[self.configuretion.userContentController addUserScript:userScript2];
[self.configuretion.userContentController addUserScript:userScript2_1];
}

#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
NSString *jsPath = [[NSBundle mainBundle] pathForResource:@"myjs" ofType:@"js"];
NSString *jsString = [[NSString alloc] initWithContentsOfFile:jsPath encoding:NSUTF8StringEncoding error:nil];

//执行js代码,用evaluateJavaScript方法。
[self.webView evaluateJavaScript:jsString completionHandler:^(id data, NSError * error) {

}];

//不执行上面的整个js内容,则下面这句js不会执行
[self.webView evaluateJavaScript:@"drawLinearGradient();" completionHandler:^(id data, NSError * error) {

}];
}

#pragma mark - WKScriptMessageHandler

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
NSLog(@"message name is %@",message.name);
NSLog(@"message body is %@",message.body);

// 如果在开始时就注入有很多的名称,那么我们就需要区分来处理
if ([message.name isEqualToString:@"TestJSMessage"] ){
//1.Alert
[self.webView evaluateJavaScript:@"alert('这是Alert');" completionHandler:^(id data, NSError * error) {
}];

//WKUserScript *userScript = [[WKUserScript alloc] initWithSource:@"function showAlert() { alert('使用用户脚本来注入 JavaScript'); }" injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
//[self.configuretion.userContentController addUserScript:userScript];

//2.confirm
[self.webView evaluateJavaScript:@"confirm('这是Confirm');" completionHandler:^(id data, NSError * error) {

}];

//3.prompt
[self.webView evaluateJavaScript:@"prompt('这是Prompt','lyl');" completionHandler:^(id data, NSError * error) {

}];
}
}
#pragma mark - WKUIDelegate
//WKUIDelegate主要实现了涉及到界面显示的回调方法:如WKWebView的改变和js相关内容
-(WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures
{
if (!navigationAction.targetFrame.isMainFrame) {
[webView loadRequest:navigationAction.request];
}
return nil;
}

// 这个方法是在HTML中调用了JS的alert()方法时,就会回调此API。
// 注意,使用了`WKWebView`后,在JS端调用alert()就不会在HTML
// 中显示弹出窗口。因此,我们需要在此处手动弹出ios系统的alert。
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:webView.URL.host message:message preferredStyle:UIAlertControllerStyleAlert];

[alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"【关闭】", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
completionHandler();// We must call back js
}]];
[self presentViewController:alertController animated:YES completion:nil];
}

// 处理JS的Confirm
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler {

// TODO We have to think message to confirm "YES"
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:webView.URL.host message:message preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:@"【确定】" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
// 点击完成后,可以做相应处理,最后再回调js端
completionHandler(YES);
}]];
[alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"【取消】", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
// 点击完成后,可以做相应处理,最后再回调js端
completionHandler(NO);
}]];
[self presentViewController:alertController animated:YES completion:nil];
}

// 处理JS的prompt
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler {

UIAlertController *alertController = [UIAlertController alertControllerWithTitle:prompt message:webView.URL.host preferredStyle:UIAlertControllerStyleAlert];
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.text = defaultText;
}];

[alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"【OK】", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
NSString *input = ((UITextField *)alertController.textFields.firstObject).text;
completionHandler(input);
}]];

[alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"【Cancel】", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
completionHandler(nil);
}]];
[self presentViewController:alertController animated:YES completion:nil];
}

@end

1、WKWebView初始化设定

上面源码的viewDidLoad中我们可以看到WKWebView初始化设定用到的几个类:

  • WKPreferences 偏好设置
  • WKUserContentController 内容控制
  • WKWebViewConfiguration 配置

WKWebView初始化需要用到WKWebViewConfiguration的实例:

1
self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:self.configuretion];

WKWebViewConfiguration需要通过WKPreferences和WKUserContentController实例来设置其包含的这两个属性:

1
2
self.configuretion.preferences = preferences;
self.configuretion.userContentController = userContentController;

2、WKWebView加载本地HTML页面

和UIWebView相同,都是使用[self.webView loadHTMLString: htmlString baseURL: nil];方法。详细内容在上面源码的loadLocalHtml中。

3、WKWebView页面加载完毕后立刻执行本地js文件中的方法

和UIWebView相似,只是代理方法,以及执行js方法的名称不同。WKWebView相比于UIWebView的[myWebView stringByEvaluatingJavaScriptFromString:@"drawLinearGradient();"];方法而言更加完善,增加了回调Block,可以在OC代码中捕捉到JS的错误及JS返回值:

1
2
3
[self.webView evaluateJavaScript:@"drawLinearGradient();" completionHandler:^(id data, NSError * error) {

}]
;

详细内容在上面源码的- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation代理方法中。

4、使用用户脚本来注入 JavaScript

WKUserScript 对象可以以 JavaScript 源码形式初始化,初始化时还可以传入是在加载之前还是结束时注入,以及脚本影响的是这个布局还是仅主要布局。于是用户脚本被加入到 WKUserContentController 中,并且以 WKWebViewConfiguration 属性传入到 WKWebView 的初始化过程中。

这个样例可以简单扩展为更为高级的页面修改方法,例如去除广告、隐藏评论等。

1
2
3
WKUserScript *userScript2 = [[WKUserScript alloc] initWithSource:@"function changeMyName() { document.getElementById('myName').innerHTML='LYL\\'s Blog'; }" injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];

[self.configuretion.userContentController addUserScript:userScript2];

详细内容在上面源码的testAddUserScript中。

注:示例代码中,提到了可以直接注入js执行方法,能够达到evaluateJavaScript的效果。经测试这种做法并不靠谱,有时不会执行(比如把这段代码挪到- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message方法中就不会执行),貌似和代码调用位置有关。为了保险起见,要执行JS方法,大家还是用应该使用evaluateJavaScript方法。

5、WKWebView处理JS调用OC

相比UIWebView笨拙的处理方式,WKWebView处理JS调用OC的逻辑过程更加清晰明了。使用过程分如下3步:

(1)OC端通过WKUserContentControlleraddScriptMessageHandler注册一个名称,来匹配js的回调。

1
[self.configuretion.userContentController addScriptMessageHandler:self name:@"TestJSMessage"];

(2)js端需要固定用window.webkit.messageHandlers.<name>.postMessage();来向OC发送消息。其中<name>必须等于第(1)步中注册的名称,否则第(3)步代理方法不会执行。

1
2
3
4
5
//此处是js代码
function callOCMethord2(){
//window.webkit.messageHandlers.<name>.postMessage();
window.webkit.messageHandlers.TestJSMessage.postMessage("这是JS传递的内容");
}

(3)OC端用WKScriptMessageHandler protocal的- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message方法来接收消息。而方法内部,通过message.name来判断代理方法中要处理哪个JS发出的OC请求。

详细内容在上面源码的testAddScriptMessageName方法中。

5、使用WKUIDelegate处理JS的弹出框

WKWebView默认是不会弹出JS对话框的,如果您需要弹出JS对话框,必须设置WKUIDelegateself.webView.UIDelegate = self;。WKUIDelegate提供了3个代理方法分别用来处理js的alert\confirm\prompt。

详细内容大家可以定位到#pragma mark - WKUIDelegate来查看,代理方法的触发在- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message方法中。

资料:

WKWebView新特性及JS交互

WKWebView

Using JavaScript with WKWebView in iOS 8