WKWebView基础
WKWebView的优势
1、更多的支持HTML5的特性
2、官方宣称的高达60fps的滚动刷新率以及内置手势
3、将UIWebViewDelegate与UIWebView拆分成了14类与3个协议,以前很多不方便实现的功能得以实现。官方文档说明
4、Safari相同的JavaScript引擎
5、占用更少的内存
6、增加加载进度属性:estimatedProgress
基本使用方法
WKWebView
有两个代理delegate,WKUIDelegate
和 WKNavigationDelegate
。
WKNavigationDelegate
WKNavigationDelegate
主要处理一些跳转、加载处理操作
WKUIDelegate
WKUIDelegate
主要处理JS脚本,确认框,警告框等。
- (void)viewDidLoad {
[super viewDidLoad];
webView = [[WKWebView alloc]init];
[self.view addSubview:webView];
[webView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view);
make.right.equalTo(self.view);
make.top.equalTo(self.view);
make.bottom.equalTo(self.view);
}];
webView.UIDelegate = self;
webView.navigationDelegate = self;
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]]];
}
加载本地的HTML文件
/*
参数1:index 是要打开的html的名称
参数2:html 是index的后缀名
参数3:HtmlFile/app/index 是文件夹的路径
*/
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"html" inDirectory:@"html"];
NSURL *pathURL = [NSURL fileURLWithPath:filePath];
[_webView loadRequest:[NSURLRequest requestWithURL:pathURL]];
#pragma mark- WKNavigationDelegate
/*
WKNavigationDelegate主要处理一些跳转、加载处理操作
*/
//页面开始加载时调用
- (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(@"----页面加载失败");
}
//接收到服务器跳转请求之后调用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation {
NSLog(@"----接收到服务器跳转请求之后调用");
}
//在收到响应后,决定是否跳转
-(void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
NSLog(@"%@",navigationResponse.response.URL.absoluteString);
//允许跳转
decisionHandler(WKNavigationResponsePolicyAllow);
//不允许跳转
//decisionHandler(WKNavigationResponsePolicyCancel);
}
//在发送请求之前,决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
NSLog(@"%@",navigationAction.request.URL.absoluteString);
//允许跳转
decisionHandler(WKNavigationActionPolicyAllow);
//不允许跳转
//decisionHandler(WKNavigationActionPolicyCancel);
}
#pragma mark- WKUIDelegate
// WKUIDelegate是web界面中有弹出警告框时调用这个代理方法,主要是用来处理使用系统的弹框来替换JS中的一些弹框的,比如: 警告框, 选择框, 输入框等
/**
webView中弹出警告框时调用, 只能有一个按钮
@param webView webView
@param message 提示信息
@param frame 可用于区分哪个窗口调用的
@param completionHandler 警告框消失的时候调用, 回调给JS
*/
- (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:nil]];
[self presentViewController:alert animated:YES completion:nil];
completionHandler();
}
/** 对应js的confirm方法
webView中弹出选择框时调用, 两个按钮
@param webView webView description
@param message 提示信息
@param frame 可用于区分哪个窗口调用的
@param completionHandler 确认框消失的时候调用, 回调给JS, 参数为选择结果: YES or NO
*/
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler {
}
// JavaScript调用prompt方法后回调的方法 prompt是js中的输入框 需要在block中把用户输入的信息传入
/** 对应js的prompt方法
webView中弹出输入框时调用, 两个按钮 和 一个输入框
@param webView webView description
@param prompt 提示信息
@param defaultText 默认提示文本
@param frame 可用于区分哪个窗口调用的
@param completionHandler 输入框消失的时候调用, 回调给JS, 参数为输入的内容
*/
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable))completionHandler {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"请输入" message:prompt preferredStyle:(UIAlertControllerStyleAlert)];
[alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
textField.placeholder = @"请输入";
}];
UIAlertAction *ok = [UIAlertAction actionWithTitle:@"确定" style:(UIAlertActionStyleDefault) handler:^(UIAlertAction * _Nonnull action) {
UITextField *tf = [alert.textFields firstObject];
completionHandler(tf.text);
}];
UIAlertAction *cancel = [UIAlertAction actionWithTitle:@"取消" style:(UIAlertActionStyleCancel) handler:^(UIAlertAction * _Nonnull action) {
completionHandler(defaultText);
}];
[alert addAction:ok];
[alert addAction:cancel];
[self presentViewController:alert animated:YES completion:nil];
}
OC与JS交互
JS 调取OC
方案1: WKUserContentController注册方法监听
js是例代码如下:
姓名:
密码:
OC代码如下:
#pragma mark- 懒加载
/*
* WKWebViewConfiguration用来初始化WKWebView的配置。
* WKPreferences配置webView能否使用JS或者其他插件等
* WKUserContentController用来配置JS交互的代码
* UIDelegate用来控制WKWebView中一些弹窗的显示(alert、confirm、prompt)。
* WKNavigationDelegate用来监听网页的加载情况,包括是否允许加载,加载失败、成功加载等一些列代理方法。
*/
- (WKWebView *)webView {
if (!_webView) {
//网页配置文件
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc]init];
//允许与网页交互
configuration.selectionGranularity = YES;
WKPreferences *preferences = [WKPreferences new];
preferences.javaScriptCanOpenWindowsAutomatically = YES;
preferences.minimumFontSize = 50;
configuration.preferences = preferences;
//注册方法
WKUserContentController *userContentController = [[WKUserContentController alloc]init];
//注册一个name为bf_jsCallNativeMethod的js方法(要记的remove)
[userContentController addScriptMessageHandler:self name:@"bf_jsCallNativeMethod"];
configuration.userContentController = userContentController;
_webView = [[WKWebView alloc]initWithFrame:self.view.bounds configuration:configuration];
_webView.navigationDelegate = self;
_webView.UIDelegate = self;
}
return _webView;
}
#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name isEqualToString:@"bf_jsCallNativeMethod"]) {
NSLog(@"这个是传过来的参数%@",message.body);
[self.webView evaluateJavaScript:@"nativeCallbackJscMothod('6666')" completionHandler:^(id _Nullable x, NSError * _Nullable error) {
NSLog(@"执行完的x==%@,error = %@",x,error.localizedDescription);
}];
}
}
/*要记得销毁是移除*/
- (void)dealloc
{
NSLog(@"--delloc--");
//这里需要注意,前面增加过的方法一定要remove掉
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"bf_jsCallNativeMethod"];
}
上面的OC代码如果认证测试一下就会发现dealloc并不会执行,这样肯定是不行的,会造成内存泄漏。原因是[userContentController addScriptMessageHandler:self name:@"bf_jsCallNativeMethod"];
这句代码造成无法释放内存。(PS:我也想到NSTimer中遇到的循环引用问题,之前总结的文章使用Weak指针还是不能释放)
改进方案
用一个新的controller来处理,新的controller再绕用delegate绕回来。
BFWKDelegateController
#import
#import
NS_ASSUME_NONNULL_BEGIN
@protocol BFWKDelegate
-(void)bf_userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message;
@end
@interface BFWKDelegateController : UIViewController
@property (weak , nonatomic) id delegate;
@end
NS_ASSUME_NONNULL_END
#import "BFWKDelegateController.h"
@interface BFWKDelegateController ()
@end
@implementation BFWKDelegateController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([self.delegate respondsToSelector:@selector(bf_userContentController:didReceiveScriptMessage:)]) {
[self.delegate bf_userContentController:userContentController didReceiveScriptMessage:message];
}
}
@end
- (WKWebView *)webView {
if (!_webView) {
//网页配置文件
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc]init];
//允许与网页交互
configuration.selectionGranularity = YES;
WKPreferences *preferences = [WKPreferences new];
preferences.javaScriptCanOpenWindowsAutomatically = YES;
preferences.minimumFontSize = 50;
configuration.preferences = preferences;
//中间的Delegate
BFWKDelegateController *bfDelegateController = [[BFWKDelegateController alloc]init];
bfDelegateController.delegate = self;
//注册方法
WKUserContentController *userContentController = [[WKUserContentController alloc]init];
//注册一个name为bf_jsCallNativeMethod的js方法
[userContentController addScriptMessageHandler:bfDelegateController name:@"bf_jsCallNativeMethod"];
configuration.userContentController = userContentController;
_webView = [[WKWebView alloc]initWithFrame:self.view.bounds configuration:configuration];
_webView.navigationDelegate = self;
_webView.UIDelegate = self;
}
return _webView;
}
/*要记得销毁是移除*/
- (void)dealloc
{
NSLog(@"--delloc--");
//这里需要注意,前面增加过的方法一定要remove掉
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"bf_jsCallNativeMethod"];
}
#pragma mark - 中转的Delegate
- (void)bf_userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
NSLog(@"name:%@ body:%@ frameInfo:%@",message.name,message.body,message.frameInfo);
}
在运行一下,当前页面销毁的时候可以执行到dealloc代码啦
注意点:
1、addScriptMessageHandler
要和removeScriptMessageHandlerForName
配套出现,否则会造成内存泄漏。
2、h5只能传一个参数,如果需要多个参数就需要用字典或者json组装。
方案2:通过拦截WKNavigationDelegate代理
通过拦截WKNavigationDelegate的代理方法,然后匹配目标字符串
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(**void** (^)(WKNavigationActionPolicy))decisionHandler;
具体实例代码如下:通过匹配是否是以js_native://alert
开头的字符串
//在发送请求之前,决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
NSURL *url = navigationAction.request.URL;
if ([[url absoluteString] hasSuffix:@"js_native://alert"]) {
[self handleJSMessage];
//不允许跳转
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
//允许跳转
decisionHandler(WKNavigationActionPolicyAllow);
}
#pragma mark - Private
-(void)handleJSMessage {
[_webView evaluateJavaScript:@"nativeCallbackJscMothod('123')" completionHandler:^(id _Nullable x, NSError * _Nullable error) {
NSLog(@"x = %@, error = %@", x, error.localizedDescription);
}];
}
方案3:苹果原生API:JavaScriptCore (iOS7.0+ 使用)
主要的两个类:
- JSContext : JS上下文(运行环境),可用对象方法去执行JS代码(evaluateScript),可通过上下文对象去获取JS里的数据(上下文对象[key]),并用JSValue对象接收
- JSValue : 用于接收JSContext对象获取的数据,可以是任意对象,方法。
实例代码如下:
JSContext *jsContent = [[JSContext alloc]init];
jsContent[@"add"] = ^(int a, int b){
NSLog(@"a+b = %d",a + b);
};
[jsContent evaluateScript:@"add(20,30)"];
注意:js代码要先被执行,才能通过上下文获取
说明:JSValue对其对应的JS值和其所属的JSContext对象都是强引用的关系。因为JSValue需要这两个东西来执行JS代码,所以JSValue会一直持有着它们。
所以,在用block时,需考虑循环引用问题
- 不要在Block中直接使用JSValue:建议把JSValue当做参数传到Block中,而不是直接在Block内部使用,这样Block就不会强引用JSValue了
- 不要在Block中直接使用JSContext:可以使用[JSContext currentContext] 方法来获取当前的Context
1、block方式:
用block定义js函数,并执行(OC调用执行js,而js是调用的oc block)
- (void)jsCallOCBlock
{
JSContext *ctx = [[JSContext alloc] init];
//OC中NSBlock对应js中Function object
ctx[@"goto"] = ^(NSString *parmStr){
//block内不要直接使用ctx,会循环引用(ctx已经引用block),若外部有JSValue,也不能在block内直接调用(JSValue强持有了ctx )
//获取JS调用参数
NSLog(@"parmStr:%@",parmStr);
//可以直接获取所有参数
NSArray *arguments = [JSContext currentArguments];
NSLog(@"%@",arguments[0]);
};
//JS执行代码,调用goto方法,并传入参数school
NSString *jsCode = @"goto('school')";
//执行
[ctx evaluateScript:jsCode];
}
2、JSExport 协议
需要在JS中生成OC对应的类,然后再通过JS调用。
方法:
通过自定义一个遵循JSExport的协议,把需要被JS访问的OC类中的属性,方法暴露给JS使用
步骤:
1、自定义协议,协议遵循JSExport的协议,协议中的属性和方法就是OC对象要暴露给JS,让JS可以直接调用
#import
#import
@protocol PersonJSExport
@property (nonatomic, strong) NSString *address;
//无参数方法
- (void)play;
//因为JS函数命名规则和OC规则不一样,所以当有多个参数时,可以使用OC提供了一个宏*JSExportAs*,指定JS应该生成什么样的函数来对应OC的方法。
//不使用JSExportAs指定关联也可以正常调用,后面直接接多个参数
//多参数方法
//不指定关联
- (void)play:(NSString *)address time:(NSString *)time;
//指定关联
JSExportAs(gotoSchool, - (void)goToSchoolWithSchoolName:(NSString *)name address:(NSString *)address);
@end
2、自定义一个遵循第一步创建的协议的OC对象,实现协议的方法
例如:Person对象
3、JS调用对象协议声明的方法
- (void)jsCallOCClass
{
// 创建Person对象,Person对象必须遵守JSExport协议
Person *p = [[Person alloc] init];
p.address = @"aaa";
JSContext *ctx = [[JSContext alloc] init];
// 在JS中生成Person对象person,并且拥有p内部的值
ctx[@"person"] = p;
// 执行JS代码
//NSString *jsCode = @"person.play()";
NSString *jsCode = @"person.play('北京天安门','now')";
//NSString *jsCode = @"person.gotoSchool('实验中学','广州')";
[ctx evaluateScript:jsCode];
}
另外,若要调用OC系统的类,例如UIView
需要同样创建协议,只是在第三步用runtime给系统类UIView添加创建的协议
class_addProtocol([UIView class], @protocol(UIViewlJSExport));
方案4: 三方库 WebViewJavaScripteBridge
OC 调取JS
方案1: WKWebView直接执行 evaluateJavaScript 方法
//原生回调JS
[self.webView evaluateJavaScript:@"nativeCallbackJscMothod('%@')" completionHandler:^(**id** **_Nullable** s, NSError * **_Nullable** error) {
NSLog(@"执行完成啦");
}];
方案2:苹果原生API:JavaScriptCore (iOS7.0+ 使用)
oc获取js变量,注:js代码要先被执行,才能通过上下文获取
- (void)ocGetJSVar
{
//定义JS代码
NSString *jsCode = @"var a = 'a'";
//创建JS运行环境
JSContext *ctx = [[JSContext alloc] init];
//!!!:执行JS代码---先执行,后面才能获取
[ctx evaluateScript:jsCode];
//获取变量
JSValue *value = ctx[**@“a"**];
//JSValue转NSString
NSString *valueStr = value.toString;
//打印结果:a
NSLog(@"%@",valueStr);
}
oc调用js方法,并获取返回结果
- (void)ocCallJSFunc
{
NSString *jsCode = @"function say(str){"
" return str; "
"}";
// 创建JS运行环境
JSContext *ctx = [[JSContext alloc] init];
// 执行JS代码
[ctx evaluateScript:jsCode];
//!!!:执行JS代码---先执行,后面才能获取
JSValue *say = ctx[@"say"];
// OC调用JS方法,获取方法返回值
JSValue *result = [say callWithArguments:@[@"hello world!"]];
// 打印结果:hello world!
NSLog(@"%@",result);
}
方案3:三方库 WebViewJavaScripteBridge
参考:
JavaScriptCore深入浅出
OC和JS调用