UIWebView 是 iOS 内置的浏览器控件,虽然现在 Apple 建议使用 WKWebView,没关系,先学习,都是通用的。

初识

UIWebView 除了能够加载远程网页、本地网页之外,还能加载很多文件,比如 pdf、pptx、mp4、html 等:

1
2
3
- (void)loadRequest:(NSURLRequest *)request;
- (void)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
- (void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType textEncodingName:(NSString *)textEncodingName baseURL:(NSURL *)baseURL;

baseURL:当网页内用到一些资源文件的时候,这个 baseURL 就起作用了,因为资源文件大多数写的都是相对路径,所以得加上 baseURL,比如:

1
2
<link rel="stylesheet" href="/css/vno.css" type="text/css">
<link rel="icon" href="/favicon.ico">

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
// 重新加载(刷新)
- (void)reload;
// 停止加载
- (void)stopLoading;
// 回退
- (void)goBack;
// 前进
- (void)goForward;
// 需要进行检测的数据类型,比如手机号、网址这些如果检测了的话,显示会有下划线等效果,而且能操作
@property(nonatomic) UIDataDetectorTypes dataDetectorTypes;
// 是否能回退
@property(nonatomic,readonly,getter=canGoBack) BOOL canGoBack;
// 是否能前进
@property(nonatomic,readonly,getter=canGoForward) BOOL canGoForward;
// 是否正在加载中
@property(nonatomic,readonly,getter=isLoading) BOOL loading;
// 是否伸缩内容至适应屏幕当前尺寸
@property(nonatomic) BOOL scalesPageToFit;

UIWebView 里面的属性和方法一看就能知道怎么使用,这里要提一下的就是关于 UIWebViewDelegate 的这个方法:

1
2
// 这个方法在 UIWebView 请求之前调用,询问是否允许访问这个 request
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;

还有,UIWebView 里有一个 UIScrollView 属性,可以对其进行一些设置操作。

既然 UIWebView 一般来说用于显示网页(其实不止,其他文件也大多支持),那么网页是由以下 3 部分组成的:

  • HTML:显示的内容
  • CSS:样式,用于美化
  • JavaScript:动态效果(如电商网站的 Banner 轮播)、事件处理、跟用户进行交互

我们可以写一个 html 文件(囊括 HTML + CSS + JavaScript),然后用 UIWebView 加载它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// index.html
<html>
<head>
<meta charset="UTF-8" />
<title>丑八怪</title>
</head>
<body>
<div>我是小良GG,电话号码:15218888888</div>
<!-- 这是注释:一般来说这些 style 应该单独写的,哈哈哈 -->
<button style="background:red; width:64px; height:44px; color:white" onclick="alert('我真帅, 666')">真帅</button>
</body>
</html>
// .m 文件
NSURL *url = [[NSBundle mainBundle] URLForResource:@"index" withExtension:@"html"];
[self.webView loadRequest:[NSURLRequest requestWithURL:url]];

简单调用 html

OC 调用 js

想在 OC 中调用 js,UIWebView 提供好了一个方法,我们只需要在网页加载完毕的时候调用即可:

1
2
3
4
5
6
7
8
9
10
11
12
// 不过 Apple 已经建议:Best practice is to adopt the WKWebView class and use its evaluateJavaScript:completionHandler: method instead.
- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;
// .m 文件,这样 UIWebView 加载完毕的时候就会弹出一个对话框
- (void)webViewDidFinishLoad:(UIWebView *)webView {
[webView stringByEvaluatingJavaScriptFromString:@"alert('小帅锅');"];
}
// 比如我们想拿到 html 的标题,可以这样做,其中 document.title; 就是 js 代码
- (void)webViewDidFinishLoad:(UIWebView *)webView {
[webView stringByEvaluatingJavaScriptFromString:@"document.title;"];
}

在 HTML 中,如果想调用 js 的方法,可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// index.html
<html>
<head>
<meta charset="UTF-8" />
<title>丑八怪</title>
<script>
// 注意:方法声明不需要返回值,但是在实现时可以返回值
function sayHello() {
return 520;
}
</script>
</head>
<body>
<div>我是小良GG,电话号码:15218888888</div>
<!-- 这是注释:一般来说这些 style 应该单独写的,哈哈哈 -->
<button style="background:red; width:64px; height:44px; color:white" onclick="alert(sayHello());">真帅</button>
</body>
</html>

如果想让 OC 调用 js 的方法,还是使用刚才的方法:

1
2
3
4
- (void)webViewDidFinishLoad:(UIWebView *)webView {
NSString *result = [webView stringByEvaluatingJavaScriptFromString:@"sayHello();"];
NSLog(@"result = %@", result);
}

js 调用 OC

js 调用 OC 是通过 UIWebView 的代理方法来完成的:

1
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;

思路很简单:就是在 html 中跳转一个『自定义请求』,然后在 OC 中上述方法拦截后就可以直接调用写好的方法了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// index.html,如果打开 location.href = "http://www.qq.com"; 这句代码,那么在点击『跳转』按钮时,网页会跳转到 qq.com 页面
<html>
<head>
<meta charset="UTF-8" />
<title>丑八怪</title>
<script>
function sayHello() {
// 让 UIWebView 跳转到 qq.com 页面(加载 qq.com 页面)
// location.href = "http://www.qq.com";
// 由于所有的请求都会被 UIWebViewDelegate 方法 webView:shouldStartLoadWithRequest:navigationType: 拦截
// 因此我们可以在这里自定义一个跳转
location.href = "ev://callMobile"; // "ev://" 是为了区别 "http://"、"file://" 等
}
</script>
</head>
<body>
<button style="background:red; width:64px; height:44px; color:white" onclick="sayHello();">跳转</button>
</body>
</html>

也就是说,在点击『跳转』按钮时,UIWebView 会发一个 “ev://callMobile” 的请求,我们只需要拦截这个请求,然后调用 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
- (void)viewDidLoad {
[super viewDidLoad];
NSURL *url = [[NSBundle mainBundle] URLForResource:@"index" withExtension:@"html"];
self.webView.delegate = self;
[self.webView loadRequest:[NSURLRequest requestWithURL:url]];
}
- (void)callMobile {
NSLog(@"call mobile");
}
// 有一个第三方框架 WebViewJavaScriptBridge 也是用了这个方法封装的
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
NSString *absoluteString = request.URL.absoluteString;
NSString *scheme = @"ev://"; // 我们在 html 中写的用于区分 http://、file:// 等的
if ([absoluteString hasPrefix:scheme]) {
// 希望调用 OC 代码,而不是加载其他请求(如跳转网页)
NSLog(@"希望调用 OC 代码");
// 得到在 js 那里写的方法名
NSString *methodName = [absoluteString substringFromIndex:scheme.length];
// 调用截取出来的方法,不过也可以直接调用 callMobile 方法也成
[self performSelector:NSSelectorFromString(methodName) withObject:nil];
return NO; // 这时,不需要发送请求
}
// 希望加载其他请求,而不是调用 OC 代码
NSLog(@"希望加载其他请求");
return YES; // 这时,需要发送请求
}

然后我发现如果 js 想要给 OC 传递参数是挺坑爹的,在 html 中带有参数,然后在 OC 中根据 “:?” 来截取:

1
location.href = "ev://callMobile:?520"; // 不过 ":?" 按你规定好的使用,比如 "#$" 等

如果要传递两个参数,还是这样拼接,然后在 OC 中调用:

1
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;

注意一个小 Tips,在调用 performSelector: 方法会报警告 PerformSelector may cause a leak because its selector is unknown,因为这个 methodName 对应的 Selector 有可能不存在,会导致 leak:

1
2
3
4
5
6
7
8
9
10
11
12
// 相当于
NSString *methodName = @"sayHello";
[self performSelector:NSSelectorFromString(methodName) withObject:nil];
// 这个时候,我们可以使用 clang 来去除这个编译警告,我们先用 ⌘B 编译,按照下图的方法寻找警告类型,然后重新编译一下
NSString *methodName = @"sayHello";
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
// 含警告的代码
[self performSelector:NSSelectorFromString(methodName) withObject:nil];
#pragma clang diagnostic pop

寻找警告类型

参考博客:http://blog.csdn.net/majiakun1/article/details/42194265

如果想要传递 3 个或者更多参数呢?可以使用 NSInvocation 来做到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)viewDidLoad {
[super viewDidLoad];
// An NSMethodSignature object records type information for the return value and parameters of a method. It is used to forward messages that the receiving object does not respond to—most notably in the case of distributed objects. You typically create an NSMethodSignature object using the NSObject methodSignatureForSelector: instance method (in macOS 10.5 and later you can also use signatureWithObjCTypes:). It is then used to create an NSInvocation object, which is passed as the argument to a forwardInvocation: message to send the invocation on to whatever other object can handle the message. In the default case, NSObject invokes doesNotRecognizeSelector:, which raises an exception. For distributed objects, the NSInvocation object is encoded using the information in the NSMethodSignature object and sent to the real object represented by the receiver of the message.
// An NSInvocation object contains all the elements of an Objective-C message: a target, a selector, arguments, and the return value. Each of these elements can be set directly, and the return value is set automatically when the NSInvocation object is dispatched.
SEL selector = @selector(sayHello);
// ViewController 的实例方法 sayHello
NSMethodSignature *methodSignature = [ViewController instanceMethodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
invocation.target = self;
invocation.selector = selector;
[invocation invoke];
// 或者
// invocation.selector = @selector(sayHello);
// [invocation invokeWithTarget:self];
}

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)sayHello:(NSString *)name mobile:(NSNumber *)age {
NSLog(@"%s, %@, %zd", __func__, name, [age integerValue]);
}
- (void)viewDidLoad {
[super viewDidLoad];
SEL selector = @selector(sayHello:mobile:);
// ViewController 的实例方法 sayHello
NSMethodSignature *methodSignature = [ViewController instanceMethodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
invocation.target = self;
invocation.selector = selector;
NSString *name = @"angelen";
NSNumber *age = [NSNumber numberWithInteger:24];
// 通过这个索引,写 n 个参数都可以,然后解析在 js 跳转的链接,分别取出参数即可
[invocation setArgument:&name atIndex:2]; // 因为索引 0 和 1 分别指示了隐藏参数 self 和 _cmd,所以从 2 开始
[invocation setArgument:&age atIndex:3];
[invocation invoke];
}

可以把这样的做法封装到一个分类(Category)里,一劳永逸:

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
// NSObject+Extension.h
#import <Foundation/Foundation.h>
@interface NSObject (Extension)
/**
调用一个方法
@param selector 方法
@param objects 参数
@return 返回值
*/
- (id)performSelector:(SEL)selector withObjects:(NSArray *)objects;
@end
// NSObject+Extension.m
#import "NSObject+Extension.h"
@implementation NSObject (Extension)
- (id)performSelector:(SEL)selector withObjects:(NSArray *)objects {
// 方法签名(方法的描述)
NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:selector];
if (signature == nil) {
// @throw [NSException exceptionWithName:@"牛逼的错误" reason:@"方法找不到" userInfo:nil];
[NSException raise:@"牛逼的错误" format:@"%@方法找不到", NSStringFromSelector(selector)];
}
// NSInvocation: 利用一个 NSInvocation 对象包装一次方法调用(方法调用者、方法名、方法参数、方法返回值)
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = self;
invocation.selector = selector;
// 设置参数
NSInteger paramsCount = signature.numberOfArguments - 2; // 除 self、_cmd 以外的参数个数
paramsCount = MIN(paramsCount, objects.count);
for (NSInteger i = 0; i < paramsCount; i++) {
id object = objects[i];
if ([object isKindOfClass:[NSNull class]]) continue;
[invocation setArgument:&object atIndex:i + 2];
}
// 调用方法
[invocation invoke];
// 获取返回值
id returnValue = nil;
if (signature.methodReturnLength) { // 有返回值类型,才去获得返回值
[invocation getReturnValue:&returnValue];
}
return returnValue;
}
@end

然后在 webView:shouldStartLoadWithRequest:navigationType: 方法中将 js 跳转的链接解析好之后直接调用 NSObject 的分类方法即可。

注意在 NSObject+Extension 中这个方法的返回值是 id 而不能写 instancetype,因为这里返回值是不确定是什么类型,所以用 id,如果用 instancetype,就是说明谁调用了这个方法,就返回谁的类型,比如 ViewController 调用了这个方法,就返回了 ViewController。