原创:知识进阶型文章
无私奉献,为国为民,创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容
推荐阅读的相关Demo
富文本ZSSRichTextEditor
iOS中UIWebView与WKWebView、JavaScript与OC交互、Cookie管理看我就够
Demo在我的Github上,欢迎下载。
NetworkAdvancedDemo
目录
- 前言
- UIWebView的使用
- OC 调用 JS中的弹框方法
- UIWebView中JS调用OC的原理就是拦截URL
- UIWebViewDelegate汇总
- JavaScriptCore的使用
- OC与JS的简单交互
- JSContext提供的异常处理机制
- JS中操作OC中的类对象
- 使用手机相册来实战一下
- WKWebView的使用
- 1、WKWebView的基本用法
- 2、交互概览
- 拦截网页的跳转链接
- OC 调用 JS方法
- WKScriptMessageHandler
- WKNavigationDelegate汇总
- WKWebView问题汇总
- JavascriptBridge
- 基本配置
- JS调用OC中的方法
- OC 调用 JS中的方法
- WKUIDelegate
- 参考文献
前言
Web
页面中的 JS 与 iOS Native
如何交互是每个 iOS 猿必须掌握的技能。而说到 Native
与 JS 交互,就不得不提一嘴Hybrid
。
Hybrid Mobile App
我对它的理解为通过Web
网络技术(如 HTML
,CSS
和 JavaScript
)与Native
相结合的混合移动应用程序。
因为 Hybrid
的灵活性(更改 Web
页面不必重新发版)以及通用性(一份 H5 玩遍所有平台)再加上门槛低(前端猿可以无痛上手开撸)的优势,所以在非核心功能模块使用 Web
通过 Hybrid
的方式来实现可能从各方面都会优于 Native
。而 Native
则可以在核心功能和设备硬件的调用上为 JS 提供强有力的支持。
1、H5
渗入 Mobile App
开发
Native APP
开发中有一个 webview
的组件(Android
中是 webview
,iOS 有 UIWebview
和 WKWebview
),这个组件可以加载 Html
文件。
在 H5 大行其道之前,webview
加载的 web
页面很单调(因为只能加载一些静态资源),自从 H5 火了之后,前端猿们开发的 H5 页面在 webview
中的表现不俗使得 H5 开发慢慢渗透到了 Mobile App
开发中来。
2、Hybrid
现状
虽然目前已经出现了 RN
和 Weex
这些使用 JS 写 Native App
的技术,但是 Hybrid
仍然没有被淘汰,市面上大多数应用都不同程度的引入了Web
页面。
UIWebView的使用
控制台输出.png运行过程.gif
UIWebView的课堂小知识
加载方法
- (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;
如果需要监听页面加载的结果,或者需要判断是否允许打开某个URL,那需要设置UIWebView的delegate
,代理只需要遵循<UIWebViewDelegate>
协议,并且在代理中实现下面的这些可选方法就可以:
@optional
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
- (void)webViewDidStartLoad:(UIWebView *)webView;
- (void)webViewDidFinishLoad:(UIWebView *)webView;
- (void)webView:(UIWebView *)webView didFailLoadWithError:(nullable NSError *)error;
OC
调用JS
self.navigationItem.title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];
缺点如下:
- 该方法不能判断调用了一个
js
方法之后,是否发生了错误。当错误发生时,返回值为nil
,而当调用一个方法本身没有返回值时,返回值也为nil
,所以无法判断是否调用成功了。 - 返回值类型为
nullable NSString *
,就意味着当调用的js
方法有返回值时,都以字符串返回,不够灵活。当返回值是一个js
的Array
时,还需要解析字符串,比较麻烦。
对于上述缺点,可以通过使用JavaScriptCore
来解决:
JSPatch
被禁事件中,最核心的就是它了。因为JavaScriptCore
的JS
到OC
的映射,可以替换各种js
方法成oc
方法,所以其动态性(配合runtime
的不安全性)也就成为了JSPatch
被Apple
禁掉的最主要原因。
其实WebKit
都有一个内嵌的js环境,一般我们在页面加载完成之后,获取js
上下文,然后通过JSContext
的evaluateScript:
方法来获取返回值。因为该方法得到的是一个JSValue
对象,所以支持JavaScript
的Array
、Number
、String
、对象等数据类型。该方法解决了stringByEvaluatingJavaScriptFromString:
返回值只是NSString
的问题。
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
//更新标题,这是上面的讲过的方法
//self.navigationItem.title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];
//获取该UIWebView的javascript上下文
JSContext *jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//这也是一种获取标题的方法。
JSValue *value = [self.jsContext evaluateScript:@"document.title"];
//更新标题
self.navigationItem.title = value.toString;
}
那么如果我执行了一个不存在的方法,比如:
[self.jsContext evaluateScript:@"document.titlexxxx"];
那么必然会报错,报错了,可以通过@property (copy) void(^exceptionHandler)(JSContext *context, JSValue *exception);
,设置该block
来获取异常。
//在调用前,设置异常回调
[self.jsContext setExceptionHandler:^(JSContext *context, JSValue *exception){
NSLog(@"%@", exception);
}];
//执行方法
JSValue *value = [self.jsContext evaluateScript:@"document.titlexxxx"];
该方法,也很好的解决了stringByEvaluatingJavaScriptFromString:
调用js
方法后,出现错误却捕获不到的缺点。
JS
调用OC
1. Custom URL Scheme
(拦截URL
)
比如页面中一个a
标签,链接如下:
<a href="darkangel://smsLogin?username=12323123&code=892845">短信验证登录</a>
在Objective-C
中,只要遵循了UIWebViewDelegate
协议,那么每次打开一个链接之前,都会触发方法:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
在该方法中,捕获该链接,并且返回NO
(阻止本次跳转),从而执行对应的OC
方法:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
//标准的URL包含scheme、host、port、path、query、fragment等
NSURL *URL = request.URL;
if ([URL.scheme isEqualToString:@"darkangel"]) {
if ([URL.host isEqualToString:@"smsLogin"]) {
NSLog(@"短信验证码登录,参数为 %@", URL.query);// 参数为 username=12323123&code=892845
return NO;
}
}
return YES;
}
参数可以是一个json
格式并且URLEncode
过的字符串,这样就可以实现复杂参数的传递(比如WebViewJavascriptBridge
)。
优点:泛用性强,可以配合h5
实现页面动态化。比如页面中一个活动链接到活动详情页,当native
尚未开发完毕时,链接可以是一个h5
链接,等到native
开发完毕时,可以通过该方法跳转到native
页面,实现页面动态化。且该方案适用于Android
和iOS
,泛用性很强。
缺点:无法直接获取本次交互的返回值,比较适合单向传参,且不关心回调的情景,比如h5
页面跳转到native
页面等。
其实,WebViewJavascriptBridge
使用的方案就是拦截URL
,为了解决无法直接获取返回值的缺点,它采用了将一个名为callback
的function
作为参数,通过一些封装,传递到OC
(js->oc
传递参数和callback
),然后在OC
端执行完毕,再通过block
来回调callback
(oc->js,传递返回值参数),实现异步获取返回值:
//JS调用OC的分享方法(当然需要OC提前注册)share为方法名,shareData为参数,后面的为回调function
WebViewJavascriptBridge.callHandler('share', shareData, function(response) {
//OC端通过block回调分享成功或者失败的结果
alert(response);
});
2、JavaScriptCore
看见个例子写得比较好,清晰易懂:有则替换,无则添加。
js
文件
//该方法传入两个整数,求和,并返回结果
function testAddMethod(a, b) {
//需要OC实现a+b,并返回
return a + b;
}
//js调用
console.log(testAddMethod(1, 5)); //output 6
oc
直接替换该方法
self.jsContext[@"testAddMethod"] = ^NSInteger(NSInteger a, NSInteger b) {
return a + b;
};
那么当在js
调用
//js调用
console.log(testAddMethod(1, 5)); //output 6, 方法为 a + b
如果oc
替换该方法为两数相乘
self.jsContext[@"testAddMethod"] = ^NSInteger(NSInteger a, NSInteger b) {
return a * b;
};
再次调用js
console.log(testAddMethod(1, 5)); //output 5,该方法变为了 a * b。
Demo实战:OC 调用 JS中的弹框方法
- 在扩展中支持委托,声明
UIWebView
@interface UIWebViewViewController ()<UIWebViewDelegate>
@property (nonatomic, strong) UIWebView *webView;
@end
- 初始化
webView
,调用loadRequest
方法加载html
的URL
- (void)viewDidLoad
{
[super viewDidLoad];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"OC调用showAlert" style:UIBarButtonItemStylePlain target:self action:@selector(didClickRightItemAction)];
self.webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
self.webView.delegate = self;
[self.view addSubview:self.webView];
NSURL *url = [[NSBundle mainBundle] URLForResource:@"index.html" withExtension:nil];;
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
[self.webView loadRequest:request];
}
- 点击导航栏右边按钮调用JS中的弹出提示框方法,传入参数
HELLO
- (void)didClickRightItemAction
{
NSString *result = [self.webView stringByEvaluatingJavaScriptFromString:@"showAlert('HELLO')()"];
NSLog(@"result == %@",result);
}
- JS中相应代码如下,需要注意的是嵌套了一个函数,内层函数拿到外层的参数,弹出提示框,并返回修改后的值给OC
function showAlert(messgae){
// 嵌套了一个函数
return function handle(){
alert('我是一个可爱的弹框,拿到了OC传来的消息: \n'+messgae);
return messgae + '传回给OC中的result';
}
}
UIWebView中JS调用OC的原理就是拦截URL
- 实现
UIWebViewDelegate
中的shouldStartLoadWithRequest
方法,该方法会加载所有请求数据,以及控制是否加载,可在此处拦截URL,实现JS调用OC
a、首先需要理解几个概念
URL.scheme
:标识 我们自己定的 一般用来APP跳转的
URL.host
:主机名,此处指方法名
URL.pathComponents
:参数
可以打印出来看看
NSLog(@"%@",request.URL.scheme); // 标识
NSLog(@"%@",request.URL.host); // 方法名
NSLog(@"%@",request.URL.pathComponents); // 参数
NavigationType
:JS响应的样式,包括
UIWebViewNavigationTypeLinkClicked, 点击
UIWebViewNavigationTypeFormSubmitted, 提交
UIWebViewNavigationTypeBackForward, 返回
UIWebViewNavigationTypeReload, 刷新
UIWebViewNavigationTypeFormResubmitted, 重复提交
UIWebViewNavigationTypeOther 其他
b、理解了后那么我们便拦截URL吧
if ([request.URL.scheme isEqualToString:@"tzedu"])
c、拦截之后自然是进入我们的主菜,JS 调用 OC
NSArray *args = request.URL.pathComponents;
NSString *methodName = args[1];
// args[1] = JSCallOC: args[2] = hello word
if ([methodName isEqualToString:@"JSCallOC:"])
{
[self JSCallOC:args[2]];
}
此处随意写了个输出方法
- (void)JSCallOC:(NSString *)str
{
// 打印结果为:这是一个OC中的方法,被JS调用了:hello word
NSLog(@"这是一个OC中的方法,被JS调用了:%@",str);
}
d、js中相应代码为
<a href="tzedu:///JSCallOC:/hello word">点击跳转响应OC方法</a>
<script>
var sbmt = document.getElementById('sbmt');
sbmt.onclick = function(event) {
window.location.href = "tzedu:///JSCallOC:/hello word";
};
</script>
UIWebViewDelegate汇总
// 开始加载
- (void)webViewDidStartLoad:(UIWebView *)webView{
NSLog(@"****************华丽的分界线****************");
NSLog(@"开始加载咯!!!!");
}
// 加载完成
- (void)webViewDidFinishLoad:(UIWebView *)webView{
NSString *titlt = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];
self.title = titlt;
NSLog(@"****************华丽的分界线****************");
NSLog(@"加载完成了咯!!!!");
}
// 加载失败
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error{
NSLog(@"****************华丽的分界线****************");
NSLog(@"加载失败了咯,为什么:%@",error);
}
JavaScriptCore的使用
控制台输出.png运行效果.gif
OC与JS的简单交互
-
引入头文件
#import <JavaScriptCore/JavaScriptCore.h>
-
在扩展中支持委托,声明
UIWebView
和JSContext
@interface ViewController ()<UIWebViewDelegate>
@property (nonatomic, strong) UIWebView *webView;
@property (nonatomic, strong) JSContext *jsContext;
@end
列举一下JavaScriptCore
常见的对象及协议:
-
JSContext
是 JS 执行上下文,你可以把它理解为 JS 运行的环境。 -
JSValue
是对JavaScript
值的引用,任何 JS 中的值都可以被包装为一个JSValue
。 -
JSManagedValu
是对JSValue
的包装,加入了“conditional retain”
。 -
SVirtualMachine
表示JavaScript
执行的独立环境。 -
JSExport
协议:实现将Objective-C
类及其实例方法,类方法和属性导出为JavaScript
代码的协议。
一般情况下我们不用手动去创建JSVirtualMachine
。因为当我们获取 JSContext
时,获取到的JSContext
从属于一个JSVirtualMachine
。
每个JSVirtualMachine
可以包含多个上下文,允许在上下文之间传递值(JSValue
对象)。 但是,每个 JSVirtualMachine
是不同的,即我们不能将一个JSVirtualMachine
中创建的值传递到另一个JSVirtualMachine
中的上下文。
JavaScriptCore API
是线程安全的 —— 例如,我们可以从任何线程创建JSValue
对象或运行 JS 脚本 - 但是,尝试使用相同 JSVirtualMachine
的所有其他线程将被阻塞。 要在多个线程上同时(并发)运行JavaScript
脚本,请为每个线程使用单独的JSVirtualMachine
实例。
注意:JavaScriptCore
只能在UIWebView
中使用,不可以在WKWebView
中使用,但是UIWebView
在IOS12之后就已经弃用了哈哈,所以大家还是直接学习WKWebView
吧
- 初始化
webView
,调用loadRequest
方法加载html
的URL
- (void)viewDidLoad {
[super viewDidLoad];
\\ 初始化webView
self.webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
self.webView.delegate = self;
[self.view addSubview:self.webView];
\\ 调用loadRequest方法加载html的URL
NSURL *url = [[NSBundle mainBundle] URLForResource:@"index.html" withExtension:nil];;
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
[self.webView loadRequest:request];
}
- 完成
UIWebViewDelegate
中webViewDidFinishLoad
方法的实现,该方法会在html
加载完成的时候调用
OC 调用 JS的变量和方法:
变量:设置当前网页标题为html中的document.title
NSString *titlt = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];
self.title = titlt;
方法:点击导航栏右边按钮调用JS中的弹出提示框方法
[self.jsContext evaluateScript:@"showAlert()"];
JS中相关的方法
function showAlert(){
alert("哈喽各位");
showMessage("你刚刚点击了弹框按钮...",arr);
}
接下来是JS调用OC中方法的具体实现的方式:
首先需要获取到JSContext
,通过webView
的valueForKeyPath
获取的,其路径为documentView.webView.mainFrame.javaScriptContext
// 获取JSContext
JSContext *jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
self.jsContext = jsContext;
接下来通过jsContext,往js中添加变量和方法
// 在js中添加全局变量arr
[self.jsContext evaluateScript:@"var arr = [5,'关羽','赵云'];"];
// 在js中添加方法
NSString *jsFunction = @"function add(a,b) {return a+b}";
[self.jsContext evaluateScript:jsFunction];
添加了,自然要取出来用
jsContext[@"showMessage"] = ^{
NSLog(@"调用OC中的showMessage具体实现");
// 因为刚才我们设置的是全局变量 可以直接获取
JSValue *arrValue = weakSelf.jsContext[@"arr"];
NSLog(@"arrValue == %@",arrValue);
int num = [[arrValue.toArray objectAtIndex:0] intValue];
num += 10;
NSLog(@"arrValue == %@ : num == %d",arrValue.toArray,num);
// 调用刚才设置的方法
JSValue *addResult = [self.jsContext[@"add"] callWithArguments:@[@2, @3]];
NSLog(@"addResult = %@", @([addResult toInt32]));// 5
// 还可以在OC 调用 JS 方法,并且传入参数
NSDictionary *dict = @{@"name":@"刘备",@"age":@22};
[[JSContext currentContext][@"ocCalljs"] callWithArguments:@[dict]];
};
这里我们通过OC中的Block
代码块,给js中的showMessage
方法添加了具体实现,并且通过JSContext
取到了JS中的变量和方法,是不是很神奇呀哈哈,让我们来看看JS中的相关代码:
<input type="button" value="弹框" onclick="showAlert()" /><br/>
// 只有调用没有实现哦
function showAlert(){
showMessage();
}
// OC 调用 JS 方法,并且传入参数
function ocCalljs(dict){
var name = dict['name'];
var age = dict['age'];
alert(name + age);
// 传回去
showDict(dict)
}
最后一步我们将JS中的方法回传过来的字典值展示到label上
self.jsContext[@"showDict"] = ^(JSValue *value) {// 拿到回传值的方法
// JS中的方法回传过来的字典值展示到label上
NSArray *args = [JSContext currentArguments];
JSValue *dictValue = args[0];
NSDictionary *dict = dictValue.toDictionary;
NSLog(@"JS中的方法回传过来的字典值:%@",dict);
dispatch_async(dispatch_get_main_queue(), ^{
weakSelf.showLabel.text = dict[@"name"];
});
};
JSContext提供的异常处理机制
JSContext也提供了异常处理机制,这里简单提下
// 异常处理
self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exception) {
context.exception = exception;
NSLog(@"exception == %@",exception);
};
JS中操作OC中的类对象
可以在JS中操作OC中的类对象,并且调用类对象的方法,很棒的功能,因为这样你就不用一个个方法去转化了,只需要转化一个类,然后调用类中方法即可
首先新建一个类JSCoreMoreObject,在该类中必须实现协议JSExport,因为在协议中声明的API都会在JS中暴露出来,才能调用,该协议中的方法需要放入JSExportAs中,其有两个参数,参数一:PropertyName可以随便起 ,参数二:Selector
// 在协议中声明的API都会在JS中暴露出来,才能调用
@protocol JSCoreMoreProtocol <JSExport>
- (void)letShowImage;
// PropertyName随便起 - Selector
JSExportAs(getSum, -(int)getSumWithNum1:(int)num1 num2:(int)num2);
@end
@interface JSCoreMoreObject : NSObject<JSCoreMoreProtocol>
@end
然后让类实现该协议
@implementation JSCoreMoreObject
- (int)getSumWithNum1:(int)num1 num2:(int)num2
{
return num1+num2;
}
@end
最后在webViewDidFinishLoad方法中实例化这个类,再试试调用它的方法
// JS 操作对象
JSCoreMoreObject *object = [[JSCoreMoreObject alloc] init];
self.jsContext[@"object"] = object;
NSLog(@"JS 操作对象: object == %d",[object getSumWithNum1:20 num2:40]);
相关JS代码如下
// JS 操作对象
function testObject(){
alert(JSCoreMoreObject.getSum(10,20));
}
使用手机相册来实战一下
完成了基本概念的理解,接下来我们通过使用手机相册来实战一下
(1)首先先添加选取相册图片和进行拍照的视图控制器的委托和成员变量
<UIWebViewDelegate,UIImagePickerControllerDelegate,UINavigationControllerDelegate>
@property (nonatomic,strong) UIImagePickerController *imagePicker;
(2)接下来实现UIImagePickerControllerDelegate
中的didFinishPickingMediaWithInfo
回调方法,
在讲解流程的过程中也顺便罗列了几个小知识点:
a、通过info
信息读取图片的数据,其中info
包含的键如下
NSString *const UIImagePickerControllerMediaType ;指定用户选择的媒体类型
NSString *const UIImagePickerControllerOriginalImage ;原始图片
NSString *const UIImagePickerControllerEditedImage ;修改后的图片
NSString *const UIImagePickerControllerCropRect ;裁剪尺寸
NSString *const UIImagePickerControllerMediaURL ;媒体的URL
NSString *const UIImagePickerControllerReferenceURL ;原件的URL
NSString *const UIImagePickerControllerMediaMetadata;当来数据来源是照相机的时候这个值才有效
所以相应代码如下
UIImage *resultImage = [info objectForKey:@"UIImagePickerControllerEditedImage"];
b、图片压缩方式
日常工作中推荐使用的是UIImageJPEGRepresentation
图压缩后节省内存,减少避免图片过多造成的卡顿现象。当然如果对图片画质有极高的要求的话,还是使用UIImagePNGRepresentation
。
偶然发现使用UIImageJPEGRepresentation
一些无背景的白色图案经过压缩之后会变成白色背景无图案的图片。因为像素低,再经压缩后,就会导致图片失真, 成为白底无图案的图片。这种情况只能使用 UIImagePNGRepresentation
用原图展示,当然这不是最好的方法。
// 0.01为这里设置的压缩系数compressionQuality
NSData *imageData = UIImageJPEGRepresentation(resultImage, 0.01);
c、先将字符串转为Data
再进行64位编码
NSString *encodedImageStr = [imageData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
d、但是编码后的字符串存在空格、回车、换行,唉,看来还得写个方法把他们移除掉
NSString *imageString = [self removeSpaceAndNewline:encodedImageStr];
// 移除空格、回车、换行
- (NSString *)removeSpaceAndNewline:(NSString *)str
{
NSString *tempStr;
// 移除空格
tempStr = [str stringByReplacingOccurrencesOfString:@" " withString:@""];
// 移除回车
tempStr = [tempStr stringByReplacingOccurrencesOfString:@"\r" withString:@""];
// 移除换行
tempStr = [tempStr stringByReplacingOccurrencesOfString:@"\n" withString:@""];
return tempStr;
}
e、哈哈,终于到了激动人心的时刻了,OC调用JS中显示图片的方法,并且向JS中传入OC的图片参数
NSString *jsFunctStr = [NSString stringWithFormat:@"showImage('%@')",imageString];
[self.jsContext evaluateScript:jsFunctStr];
对应JS文件中用来显示图片的showImage
方法的代码如下
function showImage(imageDataStr){
var tz_img = document.getElementById("tz_image");
tz_img.innerHTML = "<image style='width:200px;' src='data:image/png;base64,"+imageDataStr+"'>";
}
f、打完收工,退出相册选择器
[self dismissViewControllerAnimated:YES completion:nil];
(3)哈哈,你真以为这样就完了?年轻人,你还是太嫩了(滑天下之大稽),你忘了imagePicker
我们还没初始化呢!还有点击哪个按钮来调用打开相册方法?
我们在JS文件中定义了一个按钮用来打开相册
<input type="button" value="打开相册" onclick="openAlbumImage()" /><br />
function openAlbumImage(){
// 这里只是调用,OC中具体实现
getImage();
}
然后在OC实现的方法里顺便初始化了,直接展示,需要注意imagePicker
需要在主线程调用,而我们的webView
的加载中处于子线程,所以需要切换回主线程,否则会报紫色线程错误
// 打开相册
// Block中存在强持有self,所以自然要打破循环引用
__weak typeof(self) weakSelf = self;
self.jsContext[@"getImage"] = ^{
// imagePicker需要在主线程调用,而我们的webView的加载中处于子线程
dispatch_async(dispatch_get_main_queue(), ^{
weakSelf.imagePicker = [[UIImagePickerController alloc] init];
weakSelf.imagePicker.delegate = weakSelf;
weakSelf.imagePicker.allowsEditing = YES;
weakSelf.imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
[weakSelf presentViewController:weakSelf.imagePicker animated:YES completion:nil];
});
};
WKWebView的使用
控制台输出.png运行过程.gif
1、WKWebView的基本用法
a、简介
优势
WKWebView
是Apple于iOS 8.0推出的WebKit
中的核心控件,用来替代UIWebView
。
- 更多的支持
HTML5
的特性 - 高达
60fps
的滚动刷新率以及内置手势 - 与
Safari
相同的JavaScript
引擎 - 将
UIWebViewDelegate
拆成了一个跳转的协议WKNavigationDelegate
和一个关于UI的协议WKUIDelegate
- 可以获取加载进度:
estimatedProgress
新属性
可以通过KVO
观察这些值的变化,以便于我们做出最友好的交互。
@property (nullable, nonatomic, readonly, copy) NSString *title;// 页面的title,终于可以直接获取了
@property (nullable, nonatomic, readonly, copy) NSURL *URL;// 当前webView的URL
@property (nonatomic, readonly, getter=isLoading) BOOL loading;// 是否正在加载
@property (nonatomic, readonly) double estimatedProgress;// 加载的进度
@property (nonatomic, readonly) BOOL canGoBack;// 是否可以后退,跟UIWebView相同
@property (nonatomic, readonly) BOOL canGoForward;// 是否可以前进,跟UIWebView相同
[_webView reload];// 刷新
[_webView stopLoading];// 停止加载
[_webView goBack];// 后退函数
[_webView goForward];// 前进函数
[_webView canGoBack];// 是否可以后退
[_webView canGoForward];// 向前
[_webView isLoading];// 是否正在加载
_webView.allowsBackForwardNavigationGestures = YES;// 允许左右划手势导航
b、使用
功能点
// WebView不仅可以加载HTML页面,还支持pdf、word、txt、各种图片等等的显示
- (void)createWebView
{
NSString *htmlPath = [[NSBundle mainBundle] pathForResource:@"WKWebView" ofType:@"html"];
// 加载HTML页面
NSError *error = nil;
NSString *html = [[NSString alloc] initWithContentsOfFile:htmlPath encoding:NSUTF8StringEncoding error:&error];
NSURL *baseURL = [NSURL URLWithString:@"https://"];
[self.webView loadHTMLString:html baseURL:baseURL];
// Data
NSData *htmlData = [[NSData alloc] initWithContentsOfFile:htmlPath];
[self.webView loadData:htmlData MIMEType:@"text/html" characterEncodingName:@"UTF-8" baseURL:baseURL];
// Request
NSURL *url = [NSURL URLWithString:@""];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[self.webView loadRequest:request];
// 加载本地文件
NSURL *localURL = [NSURL fileURLWithPath:@""];
[_webView loadFileURL:localURL allowingReadAccessToURL:url];
[_webView reload];// 刷新
[_webView stopLoading];// 停止加载
[_webView goBack];// 后退函数
[_webView goForward];// 前进函数
[_webView canGoBack];// 是否可以后退
[_webView canGoForward];// 向前
[_webView isLoading];// 是否正在加载
_webView.allowsBackForwardNavigationGestures = YES;// 允许左右划手势导航
self.webView.navigationDelegate = self;
}
代理
// 在发送请求之前,决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
NSString *urlString = [[navigationAction.request URL] absoluteString];
urlString = [urlString stringByRemovingPercentEncoding];
// 用://截取字符串
NSArray *urlComps = [urlString componentsSeparatedByString:@"://"];
if (urlComps.count > 0)
{
// 获取协议头
NSString *protocolHead = [urlComps objectAtIndex:0];
NSLog(@"protocolHead = %@",protocolHead);
}
decisionHandler(WKNavigationActionPolicyAllow);
}
// 协议-开始加载时调用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation
{
NSLog(@"开始加载");
}
// 协议-当内容开始返回时调用
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation
{
NSLog(@"内容开始返回");
}
// 协议-加载完成之后调用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
NSLog(@"加载完成");
}
// 协议-加载失败时调用
- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error
{
NSLog(@"加载失败 error : %@", error.localizedDescription);
}
2、交互概览
a、OC -> JS
//执行一段js,并将结果返回,如果出错,error则不为空
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id result, NSError * _Nullable error))completionHandler;
该方法很好的解决了之前文章中提到的UIWebView
使用stringByEvaluatingJavaScriptFromString:
方法的两个缺点(1. 返回值只能是NSString
2. 报错无法捕获)。比如我想获取页面中的title
,除了直接self.webView.title
外,还可以通过这个方法:
[self.webView evaluateJavaScript:@"document.title" completionHandler:^(id _Nullable title, NSError * _Nullable error) {
NSLog(@"调用evaluateJavaScript异步获取title:%@", title);
}];
b、JS -> OC
法一:UIWebView
介绍到的URL
拦截方法一致,都是通过自定义Scheme
,在链接激活时,拦截该URL
,拿到参数,调用OC
方法。
法二:在OC中添加一个scriptMessageHandler
,则会在all frames
中添加一个js
的function
: window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
。那么当我在OC中通过如下的方法添加了一个handler
,如:
[controller addScriptMessageHandler:self name:@"currentCookies"]; //这里self要遵循协 WKScriptMessageHandler
则当我在js中调用下面的方法时:
window.webkit.messageHandlers.currentCookies.postMessage(document.cookie);
在OC中将会收到WKScriptMessageHandler
的回调:
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name isEqualToString:@"currentCookies"]) {
NSString *cookiesStr = message.body; //message.body返回的是一个id类型的对象,所以可以支持很多种js的参数类型(js的function除外)
NSLog(@"当前的cookie为: %@", cookiesStr);
}
}
当然,记得在适当的地方调用removeScriptMessageHandler
,这样就完成了一次完整的JS -> OC
的交互。
- (void)dealloc {
//记得移除
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"currentCookies"];
}
c、WKWebViewConfiguration
中的userContentController
@interface WKUserContentController : NSObject <NSCoding>
//读取添加过的脚本
@property (nonatomic, readonly, copy) NSArray<WKUserScript *> *userScripts;
//添加脚本
- (void)addUserScript:(WKUserScript *)userScript;
//删除所有添加的脚本
- (void)removeAllUserScripts;
//通过window.webkit.messageHandlers.<name>.postMessage(<messageBody>) 来实现js->oc传递消息,并添加handler
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
//删除handler
- (void)removeScriptMessageHandlerForName:(NSString *)name;
@end
拦截网页的跳转链接
- 引入头文件
#import <WebKit/WebKit.h>
- 在扩展中支持委托,声明
WKNavigationDelegate
和WKUIDelegate
@interface WKWebViewViewController ()<WKNavigationDelegate, WKUIDelegate>
@property (nonatomic, strong, readwrite) WKWebView *wkWebView;
@end
- 初始化
WKWebView
,调用loadRequest
方法加载html
的URL
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
// 调整按钮大小适应APP页面
configuration.userContentController = [self wkwebViewScalPreferences];
// 创建wkWebView
self.wkWebView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:configuration];
self.wkWebView.UIDelegate = self;
self.wkWebView.navigationDelegate = self;
[self.view addSubview:self.wkWebView];
// 加载js文件
NSString *urlStr = [[NSBundle mainBundle] pathForResource:@"index.html" ofType:nil];
NSURL *fileURL = [NSURL fileURLWithPath:urlStr];
[self.wkWebView loadFileURL:fileURL allowingReadAccessToURL:fileURL];
这里有个需要注意的地方,js的按钮在WKWebView
中不像UIWebView
那样合适,而是默认看起来很小的,所以我们需要实现一个调整按钮大小适应APP页面的方法即wkwebViewScalPreferences
,实现如下:
// 调整按钮大小适应APP页面
- (WKUserContentController *)wkwebViewScalPreferences
{
// js注入: json调整按钮大小脚本
NSString *jScript = @"var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);";
/** WKUserScript就是帮助我们完成JS注入的类,它能帮助我们在页面填充前或js填充完成后调用
* 参数1:脚本的源代码
* 参数2:脚本应注入网页的时间,是个枚举,End表示:在文档完成加载之后,但在其他子资源完成加载之前插入脚本
* 参数3:是否加入所有框架,还是只加入主框架
*/
WKUserScript *wkUserScript = [[WKUserScript alloc] initWithSource:jScript injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
WKUserContentController *wkUserContentController = [[WKUserContentController alloc] init];
[wkUserContentController addUserScript:wkUserScript];
return wkUserContentController;
}
- 实现
WKNavigationDelegate
中的decidePolicyForNavigationAction
方法,其用于拦截网页的跳转链接
a.首先我们需要获取请求的URL,测试的URL = @"lgedu://jsCallOC?username=Cooci&password=123456"
NSURL *URL = navigationAction.request.URL;
b. 接着需要获取URL Scheme
,其可以用来拦截网页的跳转链接,也就是我们前面URL中的lgedu
NSString *scheme = [URL scheme];
if ([scheme isEqualToString:@"lgedu"])// // 拦截lgedu
c. 然后获取host
即主机(这里指方法名),判断方法名是否为jsCallOC
,如果是的话就调用
这里简单解释下几个概念:
-
url
是地址, 在某个主机目录下的文件的唯一标识符(统一资源定位符url) -
domain
是域名, 比如baidu.com
就是域名,而其对应的ip地址指向了百度的服务器 -
host
是主机 - 默认情况
http
协议是80端口https
协议是443端口
NSString *host = [URL host];
// 方法名为jsCallOC
if ([host isEqualToString:@"jsCallOC"])
{
// 解析URL
}
else
{
NSLog(@"不明方法 %@",host);
}
d. 有点儿累了,好困啊~,最后我们需要解析一下传入的URL,此处需要写一个方法decoderUrl
用来解析,具体实现如下:
// 解析URL地址
- (NSMutableDictionary *)decoderUrl:(NSURL *)URL
{
// URL = @"lgedu://jsCallOC?username=谢佳培&password=123456"
// 分开参数
NSArray *params = [URL.query componentsSeparatedByString:@"&"];
NSMutableDictionary *tempDic = [NSMutableDictionary dictionary];
for (NSString *param in params)
{
// 分开键值对
NSArray *dictArray = [param componentsSeparatedByString:@"="];
if (dictArray.count > 1)
{
// 取值, 中文解码
NSString *decodeValue = [dictArray[1] stringByRemovingPercentEncoding];
// 解码后放入tempDic
[tempDic setObject:decodeValue forKey:dictArray[0]];
}
}
return tempDic;
}
简单解释下,该函数首先获取URL.query
即请求参数,将其根据&
来分开,然后遍历每对参数,再根据=
分开键值对
此处存在一个小问题,因为网络请求会拼接中文参数,用户名登陆等很多地方会用到中文,所以需要针对中文进行编码和解码,举两个比较清晰的例子:
编码:
NSString* hStr =@"你好啊";
NSString* hString = [hStr stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
NSLog(@"hString === %@",hString); // hString === %E4%BD%A0%E5%A5%BD%E5%95%8A
解码:
NSString*str3 =@"\u5982\u4f55\u8054\u7cfb\u5ba2\u670d\u4eba\u5458\uff1f";
NSString*str5 = [str3 stringByRemovingPercentEncoding];
NSLog(@"str5 ==== %@",str5);// str5 ==== 如何联系客服人员?
看懂了上面的例子就很容易理解[dictArray[1] stringByRemovingPercentEncoding];
这句代码了,就是中文解码后再取值,最后根据key
放入tempDic
并返回
e. 好,完成了支线任务,回到主线情节,来到我们之前decidePolicyForNavigationAction
方法中解析URL的地方,这时候就知道该从何下手了,即取出解析后的值,使用即可(打印出我的大名哈哈😄)
// 解析URL
NSMutableDictionary *temDict = [self decoderUrl:URL];
NSString *username = [temDict objectForKey:@"username"];
NSString *password = [temDict objectForKey:@"password"];
// 用户名和密码:谢佳培------123456
NSLog(@"用户名和密码:%@------%@",username,password);
- JS中相关代码如下:
<input type="button" value="拦截导航" onclick="interceptNavigation()" /> <br />
function interceptNavigation(){
alert('点击了拦截按钮,调用loadURL方法获得URL');
loadURL("lgedu://jsCallOC?username=谢佳培&password=123456");
}
function loadURL(url) {
var iFrame;
iFrame = document.createElement("iframe");
iFrame.setAttribute("src", url);
iFrame.setAttribute("style", "display:none;");
iFrame.setAttribute("height", "0px");
iFrame.setAttribute("width", "0px");
iFrame.setAttribute("frameborder", "0");
document.body.appendChild(iFrame);
iFrame.parentNode.removeChild(iFrame);
iFrame = null;
}
OC 调用 JS方法
前提:
实现WKNavigationDelegate
中的didFinishNavigation
方法,其在页面加载完成之后调用,我们在其中定义了一个全局变量arr
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
// 导航栏标题
self.title = webView.title;
// 定义一个全局变量 arr
NSString *jsStr = @"var arr = [5, '谢佳培', 'xiejiapei']; ";
[self.wkWebView evaluateJavaScript:jsStr completionHandler:^(id _Nullable result, NSError * _Nullable error) {
// 页面加载完成之后调用, 定义一个全局变量 arr: (null)----(null)
NSLog(@"页面加载完成之后调用, 定义一个全局变量 arr: %@----%@",result, error);
}];
}
- 首先我们添加了个导航栏右按钮,点击它来通过OC调用
showAlert
方法
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"OC调用showAlert" style:UIBarButtonItemStylePlain target:self action:@selector(didClickRightItemAction)];
- 接着实现
didClickRightItemAction
方法
解释下下面的方法:
a、showAlert
中调用了js的alert('我是一个可爱的弹框 \n'+messgae+'\n'+arr[1]);
该方法会弹出js的提示框,显示相应信息,其中arr[1]来自在我们在页面加载完成后定义的全局变量arr,最终输出的信息是我是一个可爱的弹框 嗨,这是通过OC调用showAlert方法哦 谢佳培
b、通过evaluateJavaScript
来使OC 调用 JS方法,会输出token
,解释下这个东东
Token
是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token
便将此Token
返回给客户端,以后客户端只需带上这个Token
前来请求数据即可,无需再次带上用户名和密码。
使用Token
的目的:为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。
- (void)didClickRightItemAction
{
// JS方法
NSString *jsStr = [NSString stringWithFormat:@"showAlert('%@')",@"嗨,这是通过OC调用showAlert方法哦"];
// OC 调用 JS方法
[self.wkWebView evaluateJavaScript:jsStr completionHandler:^(id _Nullable result, NSError * _Nullable error) {
NSLog(@"OC 调用 JS方法: %@----%@",result, error);
}];
}
- JS中相应代码
function showAlert(messgae){
alert('我是一个可爱的弹框 \n'+messgae+'\n'+arr[1]);
return "token";
}
- 补充一点,因为JS的弹出框的样式之丑陋简直令人发指,所以此处使用OC样式的弹出警告框,覆盖JS的丑陋样式,实现
WKUIDelegate
中的runJavaScriptAlertPanelWithMessage
方法即可
// 警告框
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
{
// 使用OC样式的弹出警告框,覆盖JS的丑陋样式
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提醒" message:message preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *knowAction = [UIAlertAction actionWithTitle:@"知道了" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
// 回调完成
completionHandler();
}];
[alert addAction:knowAction];
[self presentViewController:alert animated:YES completion:nil];
}
WKScriptMessageHandler
- 加了一个新的委托
WKScriptMessageHandler
<WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler>
- 敲黑板!!!特别重要的关键点来了!
a、html5
中需要添加window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
方法,来实现JS与OC之间的桥梁,此处向JS发送消息,messgaeOC
为用于JS和OC沟通的消息名称,消息内容在JS中
b、但是postMessage
方法有个问题,即循环引用:self - webView - configuration - userContentController - self
,所以需要我们根据name
移除所注入的scriptMessageHandler
来打破循环引用, 否则不会调用到dealloc
方法
// 循环引用:self - webView - configuration - userContentController - self
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// html5中需要添加window.webkit.messageHandlers.<name>.postMessage(<messageBody>)方法,来实现js与oc之间的桥梁
[self.wkWebView.configuration.userContentController addScriptMessageHandler:self name:@"messgaeOC"];
}
// 需要根据name移除所注入的scriptMessageHandler来打破循环引用, 否则不会调用到dealloc方法
- (void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
[self.wkWebView.configuration.userContentController removeScriptMessageHandlerForName:@"messgaeOC"];
}
- (void)dealloc
{
NSLog(@"dealloc:溜了溜了");
}
- 最后一步,实现
WKScriptMessageHandler
中的didReceiveScriptMessage
方法接收JS中的消息
// WKWebView收到ScriptMessage时回调此方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
if (message.name)
{
// OC 层面的消息
}
// message == messgaeOC --- 青春气息
NSLog(@"消息是:%@ --- %@",message.name,message.body);
}
- JS中相关代码如下
<input type="button" value="messgaeHandle" onclick="messgaeHandle()" /><br />
function messgaeHandle(){
alert('给messageHandlers页面发送了“青春气息”');
window.webkit.messageHandlers.messgaeOC.postMessage("青春气息");
}
WKNavigationDelegate汇总
啊嗷嗷,总算结束了!写文章真是件累人的活啊,特别是技术博客
\\ 请求之前,决定是否要跳转:用户点击网页上的链接,需要打开新页面时,将先调用这个方法
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
\\ 接收到相应数据后,决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
\\ 页面开始加载时调用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
\\ 主机地址被重定向时调用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
\\ 页面加载失败时调用
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified
\\ 当内容开始返回时调用
- (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation*)navigation;
\\ 页面加载完毕时调用
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;
\\ 跳转失败时调用
- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
\\ 如果需要证书验证,与使用AFN进行HTTPS证书验证是一样的
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *__nullable credential))completionHandler;
\\ web内容处理中断时会触发
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView NS_AVAILABLE(10_11, 9_0);
WKWebView问题汇总
这些问题我没有遇到过哈,只是先提前罗列下,以后遇到了再回过头细细品味:
白屏问题
当WKWebView
加载的网页占用内存过大时,会出现白屏现象。解决方案是:
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView {
[webView reload]; //刷新就好了
}
有时白屏,不会调用该方法,具体的解决方案是:
比如,最近遇到在一个高内存消耗的H5页面上
present
系统相机,拍照完毕后返回原来页面的时候出现白屏现象(拍照过程消耗了大量内存,导致内存紧张,WebContent Process
被系统挂起),但上面的回调函数并没有被调用。在WKWebView
白屏的时候,另一种现象是webView.titile
会被置空, 因此,可以在viewWillAppear
的时候检测webView.title
是否为空来reload
页面。(出自WKWebView 那些坑)
evaluateJavaScript:completionHandler: 只支持异步问题
该方法是异步回调,这个一看方法的声明便知。可能有小伙伴就是需要同步获取返回值,有没有办法呢?答案是没有。可能你会说用信号量dispatch_semaphore_t
。好吧,可能你会这么写~
__block id cookies;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self.webView evaluateJavaScript:@"document.cookie" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
cookies = result;
dispatch_semaphore_signal(semaphore);
}];
//等待三秒,接收参数
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC));
//打印cookie,肯定为空,因为足足等了3s,dispatch_semaphore_signal都没有起作用
NSLog(@"cookie的值为:%@", cookies);
笔者故意只等待了3s,如果你等待DISPATCH_TIME_FOREVER
,恭喜你,程序不会Crash
,但界面卡死了。笔者测试的结果是,NSLog
的触发时间要早于completionHandler
回调,不论你等多久,它都会打印null
。所以当你永久等待时,就卡死了。这里的缘由,笔者不太清楚,有搞清楚的小伙伴可以帮忙指点一下,谢谢~所以还是老实的接受异步回调吧,不要用信号来搞成同步,会卡死的。
自定义contentInset刷新时页面跳动的bug
这么写就OK了,通过KVC设置私有变量的值,笔者用了半年了,过Apple审核没问题,不用担心。
self.webView.scrollView.contentInset = UIEdgeInsetsMake(64, 0, 49, 0);
//史诗级神坑,为何如此写呢?参考https://opensource.apple.com/source/WebKit2/WebKit2-7600.1.4.11.10/ChangeLog
[self.webView setValue:[NSValue valueWithUIEdgeInsets:self.webView.scrollView.contentInset] forKey:@"_obscuredInsets"]; //kvc给WKWebView的私有变量_obscuredInsets设置值
NSURLProtocol问题
WKWebView
不同于UIWebView
,其实并不支持NSURLProtocol
。如果想拦截,可以通过调用私有Api
。
+ [WKBrowsingContextController registerSchemeForCustomProtocol:]
此方法缺点也很多,笔者这里不推荐大家使用,毕竟调用私有Api
是Apple
禁止的。况且,真的必须使用NSURLProtocol
的话,还是用UIWebView
吧。
WKScriptMessageHandler问题
1、该方法还是没有办法直接获取返回值。
2、通过window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
传递的messageBody
中不能包含js的function
,如果包含了function
,那么 OC端将不会收到回调。
对于问题1,我们可以采用异步回调的方式,将返回值返回给js
。
对于问题2,一般js
的参数中包含function
是为了异步回调,这里我们可以把js
的function
转换为字符串,再传递给OC。
比如js
端实现了如下的方法:
/**
* 分享方法,并且会异步回调分享结果
* @param {对象类型} shareData 一个分享数据的对象,包含title,imgUrl,link以及一个回调function
* @return {void} 无同步返回值
*/
function shareNew(shareData) {
//这是该方法的默认实现,上篇文章中有所提及
var title = shareData.title;
var imgUrl = shareData.imgUrl;
var link = shareData.link;
var result = shareData.result;
//do something
//这里模拟异步操作
setTimeout(function() {
//2s之后,回调true分享成功
result(true);
}, 2000);
//用于WKWebView,因为WKWebView并没有办法把js function传递过去,因此需要特殊处理一下
//把js function转换为字符串,oc端调用时 (<js function string>)(true); 即可
shareData.result = result.toString();
window.webkit.messageHandlers.shareNew.postMessage(shareData);
}
function test() {
//清空分享结果
shareResult.innerHTML = "";
//调用时,应该
shareNew({
title: "title",
imgUrl: "http://img.dd.com/xxx.png",
link: location.href,
result: function(res) {
//这里shareResult 等同于 document.getElementById("shareResult")
shareResult.innerHTML = res ? "success" : "failure";
}
});
}
在html
页面中我定义了一个a
标签来触发test()
函数:
<a href="javascript:void(0);" onclick="test()">测试新分享</a>
在OC端,实现如下:
//首先别忘了,在configuration中的userContentController中添加scriptMessageHandler
[controller addScriptMessageHandler:self name:@"shareNew"]; //记得适当时候remove哦
//点击a标签时,则会调用下面的方法
#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name isEqualToString:@"shareNew"]) {
NSDictionary *shareData = message.body;
NSLog(@"shareNew分享的数据为: %@", shareData);
//模拟异步回调
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//读取js function的字符串
NSString *jsFunctionString = shareData[@"result"];
//拼接调用该方法的js字符串
NSString *callbackJs = [NSString stringWithFormat:@"(%@)(%d);", jsFunctionString, NO]; //后面的参数NO为模拟分享失败
//执行回调
[self.webView evaluateJavaScript:callbackJs completionHandler:^(id _Nullable result, NSError * _Nullable error) {
if (!error) {
NSLog(@"模拟回调,分享失败");
}
}];
});
}
}
那么当我点击a标签时,html
页面上过2s,会显示success
,然后再过2s,会显示failure
。
我们来简单分析一下,点击之后,触发了test()
函数,test()
中封装了对share()
函数的调用,且传了一个对象作为参数,对象中result
字段对应的是个匿名函数,紧接着share()
函数调用,其中的实现是2s过后,result(true);
模拟js
异步实现异步回调结果,分享成功。同时share()
函数中,因为通过scriptMessageHandler
无法传递function
,所以先把shareData
对象中的result
这个匿名function
转成String
,然后替换shareData
对象的result
属性为这个String
,并回传给OC,OC这边对应JS对象的数据类型是NSDictionary
,我们打印并得到了所有参数,同时,把result
字段对应的js function String
取出来。这里我们延迟4s回调,模拟Native
分享的异步过程,在4s后,也就是js中显示success
的2s过后,调用js的匿名function
,并传递参数(分享结果)。调用一个js function
的方法是 functionName(argument);
,这里由于这个js的function
已经是一个String
了,所以我们调用时,需要加上(),如(functionString)(argument);
因此,最终我们通过OC -> JS 的evaluateJavaScript:completionHandler:
方法,成功完成了异步回调,并传递给js一个分享失败的结果。
上面的描述看起来很复杂,其实就是先执行了JS的默认实现,后执行了OC的实现。上面的代码展示了如何解决scriptMessageHandler
的两个问题,并且实现了一个 JS -> OC、OC -> JS 完整的交互流程。
JavascriptBridge的使用
控制台输出.png运行过程.gif
懒总是第一创新力,是不是觉得之前的三种方式操作起来都稍微有点麻烦?于是乎,我们在这里介绍一个框架JavascriptBridge
,它的强大之处就在于底部实现了navgationDelegate
中的拦截机制,而且支持WKWebView & UIWebView
,我们只需要处理自己的业务逻辑就行了,先简单介绍下:(官方语言总是绕口令又那么死板僵硬,大学的离散数学把我都给绕晕了唉)
JavaScript
是运行在一个单独的 JS Context
中(如WebView
的 Webkit
引擎、IOS
的JavaScriptCore
),这些 Context
与原生运行环境的天然隔离。WebViewJavaScriptBridge
实现Native
与 JS
通信的基本原理是:
把 OC 的方法注册到桥梁中,让 JS 去调用。
把 JS 的方法注册在桥梁中,让 OC 去调用。
下文以WKWebView
为例:
基本配置
- 导入框架
<WebKit/WebKit.h>
和<WebViewJavascriptBridge.h>
- 在扩展中支持委托
WKUIDelegate
,注意不用支持WKNavigationDelegate
哦,声明WKWebView
和WebViewJavascriptBridge
@interface JavascriptBridgeViewController ()<WKUIDelegate>
@property (strong, nonatomic) WKWebView *wkWebView;
@property (strong, nonatomic) WebViewJavascriptBridge *webViewJavascriptBridge;
@end
- 初始化
webView
,调用loadRequest
方法加载html的URL
,和之前一样就不解释了,不懂的可以看WKWebView
部分
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
configuration.userContentController = [self wkwebViewScalPreferences];
self.wkWebView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:configuration];
self.wkWebView.UIDelegate = self;
[self.view addSubview:self.wkWebView];
NSString *urlStr = [[NSBundle mainBundle] pathForResource:@"index.html" ofType:nil];
NSURL *fileURL = [NSURL fileURLWithPath:urlStr];
[self.wkWebView loadFileURL:fileURL allowingReadAccessToURL:fileURL];
- 接下来主角帅气登场,传入
wkWebView
来初始化webViewJavascriptBridge
// 传入wkWebView来初始化webViewJavascriptBridge,框架会自动判断传入的是WKWebView还是UIWebView作出处理
self.webViewJavascriptBridge = [WebViewJavascriptBridge bridgeForWebView:self.wkWebView];
这里稍微解释下,关于传入后框架会自动判断传入的是WKWebView
还是UIWebView
作出处理
框架中相应源代码如下:
// 外部调用的方法
+ (instancetype)bridgeForWebView:(id)webView {
return [self bridge:webView];
}
// 内部实际实现的方法
+ (instancetype)bridge:(id)webView {
#if defined supportsWKWebView
// 判断是否是WKWebView
if ([webView isKindOfClass:[WKWebView class]]) {
return (WebViewJavascriptBridge*) [WKWebViewJavascriptBridge bridgeForWebView:webView];
}
#endif
// 判断是否是UIWebView
// #define WVJB_WEBVIEW_TYPE UIWebView
if ([webView isKindOfClass:[WVJB_WEBVIEW_TYPE class]]) {
WebViewJavascriptBridge* bridge = [[self alloc] init];
[bridge _platformSpecificSetup:webView];
return bridge;
}
JS调用OC中的方法
- 在之前的
WKWebView
中此处是通过NavgationDelegate
中的拦截机制实现的,而在这里我们只需要注册想要调用的方法名,然后在handler
中实现该方法即可,框架内部帮我们实现了该代理并且会自动切换到主线程让我们拿到数据后更新UI,其中data
是JS回传给我们的该方法会用到的实际数据,其实就是入参,responseCallback
是调用完OC之后的回调
// JS调用OC中的方法
[self.webViewJavascriptBridge registerHandler:@"jsCallsOC" handler:^(id data, WVJBResponseCallback responseCallback) {
// 自动切换到主线程让我们拿到数据后更新UI
NSLog(@"currentThread == %@",[NSThread currentThread]);
// `data`是JS回传给我们的该方法会用到的实际数据
// responseCallback在JS中对应的是alert(response);
NSLog(@"data == %@ -- %@",data,responseCallback);
}];
此处需要注意两点:
a、封装SDK时候的思想:封装SDK不要让外界出现安全隐患,最好回调信息后便将其置于主线程,这样虽然慢点,但是安全,不会让使用者认为该框架有安全隐患,因为如果还处于子线程,外界不小心用拿到的数据更新了UI出错了就是你框架背锅。
b、让我们做个好奇宝宝探究下该框架内部是如何实现拦截的,浅尝辄止不深究
首先也是通过实现NavgationDelegate
中的拦截方法shouldStartLoadWithRequest
来实现的
// 也实现了NavgationDelegate中的拦截方法shouldStartLoadWithRequest
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
if (webView != _webView) { return YES; }
NSURL *url = [request URL];
// 该方法拦截了URL
if ([_base isWebViewJavascriptBridgeURL:url]) {
再进入isWebViewJavascriptBridgeURL
一探究竟
- (BOOL)isBridgeLoadedURL:(NSURL*)url {
// 也是判断host和Scheme
NSString* host = url.host.lowercaseString;
return [self isSchemeMatch:url] && [host isEqualToString:kBridgeLoaded];
}
最后看看isSchemeMatch
是怎么判断Scheme
的
- (BOOL)isSchemeMatch:(NSURL*)url {
NSString* scheme = url.scheme.lowercaseString;
return [scheme isEqualToString:kNewProtocolScheme] || [scheme isEqualToString:kOldProtocolScheme];
}
其中kNewProtocolScheme
和kOldProtocolScheme
如下:
#define kOldProtocolScheme @"wvjbscheme"
#define kNewProtocolScheme @"https"
很多人就纳闷了,漏了一个很关键的地方,我们的URL在哪呢,之前都在JS文件里添加的,现在没影了?不知道URL,我怎么比对Scheme
和Host
?所以该框架在JS中需要实现该方法
// 创建一个不可见的iframe来加载初始化链接URL(https://_bridge_loaded_)
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];// 创建一个 WVJBCallbacks 全局属性数组,并将 callback 插入到数组中。
var WVJBIframe = document.createElement('iframe');// 创建一个 iframe 元素
// display = 'none'创建一个不可见的iframe
WVJBIframe.style.display = 'none';// 不显示
WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__';//设置 iframe 的 src 属性
document.documentElement.appendChild(WVJBIframe);/ 把 iframe 添加到当前文导航上
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
看着很复杂对吧,对你来说只需要复制粘贴即可哈哈,如果想吃透框架原理的可以研究下,大致就是创建一个不可见的iframe
来加载初始化链接URL(https://_bridge_loaded_)
,贴张流程图:
我真是太尽职尽责了,为自己感动到哭泣!
总的来说,还是和我们之前
WKWebView
那做的操作一样,只是多封装了几层,考虑了更多情况更完善罢了
- 支线任务做完了,回到主角的剧情路线,看看JS中的相应代码部分
通过callHandler
调用OC中jsCallsOC
方法,并传入参数
<input type="button" value="JS调用OC中方法" onclick="click()" /> <br />
// JS调用OC
function click(){
// function(response)是个返回信息
WebViewJavascriptBridge.callHandler('jsCallsOC', {'谢佳培': '22'}, function(response) {
alert(response);
})
}
OC 调用 JS中的方法
分三种情况,对应使用即可
// 如果不需要参数,不需要回调,使用这个
[self.webViewJavascriptBridge callHandler:@"OCCallJSFunction"];
// 如果需要参数,不需要回调,使用这个
[self.webViewJavascriptBridge callHandler:@"OCCallJSFunction" data:@"今天天气要下雨了吧,我看外面在打雷"];
// 如果既需要参数,又需要回调,使用这个
[self.webViewJavascriptBridge callHandler:@"OCCallJSFunction" data:@"今天天气要下雨了吧,我看外面在打雷" responseCallback:^(id responseData) {
NSLog(@"currentThread == %@",[NSThread currentThread]);
NSLog(@"调用完JS后的回调:%@",responseData);
}];
相应JS中的Code如下:
// OC调用JS
setupWebViewJavascriptBridge(function(bridge) {
// ‘OCCallJSFunction’作为标识符,找到在JS中想要调用的方法
bridge.registerHandler('OCCallJSFunction', function(data, responseCallback) {
alert('JS方法被调用:'+data);
responseCallback('js执行过了');
})
})
WKUIDelegate
完美,最后再来点扩展,既然NavgationDelegate
被框架自动实现了,那么WKUIDelegate
也就是说我们还是可以自己实现的啰,所以如果你要在VC中实现 UIWebView的代理方法 就实现下面的代码(否则省略)
[self.webViewJavascriptBridge setWebViewDelegate:self];
此处我们用它来实现了弹出框的OC式
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提醒" message:message preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"知道了" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
completionHandler();
}]];
[self presentViewController:alert animated:YES completion:nil];
}
网友评论