WKWebView基礎(chǔ)
WKWebView的優(yōu)勢
1、更多的支持HTML5的特性
2溺蕉、官方宣稱的高達60fps的滾動刷新率以及內(nèi)置手勢
3、將UIWebViewDelegate與UIWebView拆分成了14類與3個協(xié)議,以前很多不方便實現(xiàn)的功能得以實現(xiàn)观蓄。官方文檔說明
4僵腺、Safari相同的JavaScript引擎
5、占用更少的內(nèi)存
6、增加加載進度屬性:estimatedProgress
基本使用方法
WKWebView
有兩個代理delegate,WKUIDelegate
和 WKNavigationDelegate
结胀。
WKNavigationDelegate
WKNavigationDelegate
主要處理一些跳轉(zhuǎn)赞咙、加載處理操作
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文件
/*
參數(shù)1:index 是要打開的html的名稱
參數(shù)2:html 是index的后綴名
參數(shù)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主要處理一些跳轉(zhuǎn)、加載處理操作
*/
//頁面開始加載時調(diào)用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
NSLog(@"----頁面開始加載");
}
//當(dāng)內(nèi)容開始返回時調(diào)用
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
NSLog(@"----頁面返回內(nèi)容");
}
//頁面加載完成時調(diào)用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
NSLog(@"----頁面加載完成");
}
//頁面加載失敗時調(diào)用
- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error {
NSLog(@"----頁面加載失敗");
}
//接收到服務(wù)器跳轉(zhuǎn)請求之后調(diào)用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation {
NSLog(@"----接收到服務(wù)器跳轉(zhuǎn)請求之后調(diào)用");
}
//在收到響應(yīng)后秸抚,決定是否跳轉(zhuǎn)
-(void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
NSLog(@"%@",navigationResponse.response.URL.absoluteString);
//允許跳轉(zhuǎn)
decisionHandler(WKNavigationResponsePolicyAllow);
//不允許跳轉(zhuǎn)
//decisionHandler(WKNavigationResponsePolicyCancel);
}
//在發(fā)送請求之前速和,決定是否跳轉(zhuǎn)
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
NSLog(@"%@",navigationAction.request.URL.absoluteString);
//允許跳轉(zhuǎn)
decisionHandler(WKNavigationActionPolicyAllow);
//不允許跳轉(zhuǎn)
//decisionHandler(WKNavigationActionPolicyCancel);
}
#pragma mark- WKUIDelegate
// WKUIDelegate是web界面中有彈出警告框時調(diào)用這個代理方法,主要是用來處理使用系統(tǒng)的彈框來替換JS中的一些彈框的,比如: 警告框, 選擇框, 輸入框等
/**
webView中彈出警告框時調(diào)用, 只能有一個按鈕
@param webView webView
@param message 提示信息
@param frame 可用于區(qū)分哪個窗口調(diào)用的
@param completionHandler 警告框消失的時候調(diào)用, 回調(diào)給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();
}
/** 對應(yīng)js的confirm方法
webView中彈出選擇框時調(diào)用, 兩個按鈕
@param webView webView description
@param message 提示信息
@param frame 可用于區(qū)分哪個窗口調(diào)用的
@param completionHandler 確認框消失的時候調(diào)用, 回調(diào)給JS, 參數(shù)為選擇結(jié)果: YES or NO
*/
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler {
}
// JavaScript調(diào)用prompt方法后回調(diào)的方法 prompt是js中的輸入框 需要在block中把用戶輸入的信息傳入
/** 對應(yīng)js的prompt方法
webView中彈出輸入框時調(diào)用, 兩個按鈕 和 一個輸入框
@param webView webView description
@param prompt 提示信息
@param defaultText 默認提示文本
@param frame 可用于區(qū)分哪個窗口調(diào)用的
@param completionHandler 輸入框消失的時候調(diào)用, 回調(diào)給JS, 參數(shù)為輸入的內(nèi)容
*/
- (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 調(diào)取OC
方案1: WKUserContentController注冊方法監(jiān)聽
js是例代碼如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<table width="80%" align="center" border="0">
<tr>
<td width="30%" align="right">姓名:</td>
<td align="left">
<input type="text" name="username">
</td>
</tr>
<tr>
<td width="30%" align="right">密碼:</td>
<td align="left">
<input type="text" name="password" >
</td>
</tr>
<tr >
<td align="right">
<input type="button" name="submit" value="提交" onclick="jsCallNativeMethod()">
</td>
<td align="left">
<input type="button" name="login" value="登錄" onclick="bf_jsCallNativeMethod()">
</td>
<td></td>
</tr>
</table>
<script type="text/javascript">
function jsCallNativeMethod(){
location.href = "js_native://alert";
}
function nativeCallbackJscMothod(arguments) {
alert('原生調(diào)用js方法 傳來的參數(shù) = ' + arguments);
}
function bf_jsCallNativeMethod() {
window.webkit.messageHandlers.bf_jsCallNativeMethod.postMessage('我的參數(shù)222');
}
function add(a , b) {
return a + b
}
</script>
</body>
</html>
OC代碼如下:
#pragma mark- 懶加載
/*
* WKWebViewConfiguration用來初始化WKWebView的配置。
* WKPreferences配置webView能否使用JS或者其他插件等
* WKUserContentController用來配置JS交互的代碼
* UIDelegate用來控制WKWebView中一些彈窗的顯示(alert剥汤、confirm颠放、prompt)。
* WKNavigationDelegate用來監(jiān)聽網(wǎng)頁的加載情況吭敢,包括是否允許加載碰凶,加載失敗、成功加載等一些列代理方法鹿驼。
*/
- (WKWebView *)webView {
if (!_webView) {
//網(wǎng)頁配置文件
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc]init];
//允許與網(wǎng)頁交互
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(@"這個是傳過來的參數(shù)%@",message.body);
[self.webView evaluateJavaScript:@"nativeCallbackJscMothod('6666')" completionHandler:^(id _Nullable x, NSError * _Nullable error) {
NSLog(@"執(zhí)行完的x==%@,error = %@",x,error.localizedDescription);
}];
}
}
/*要記得銷毀是移除*/
- (void)dealloc
{
NSLog(@"--delloc--");
//這里需要注意欲低,前面增加過的方法一定要remove掉
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"bf_jsCallNativeMethod"];
}
上面的OC代碼如果認證測試一下就會發(fā)現(xiàn)dealloc并不會執(zhí)行,這樣肯定是不行的畜晰,會造成內(nèi)存泄漏砾莱。原因是[userContentController addScriptMessageHandler:self name:@"bf_jsCallNativeMethod"];
這句代碼造成無法釋放內(nèi)存。(PS:我也想到NSTimer中遇到的循環(huán)引用問題凄鼻,之前總結(jié)的文章使用Weak指針還是不能釋放)
改進方案
用一個新的controller來處理,新的controller再繞用delegate繞回來腊瑟。
BFWKDelegateController
#import <UIKit/UIKit.h>
#import <WebKit/WebKit.h>
NS_ASSUME_NONNULL_BEGIN
@protocol BFWKDelegate <NSObject>
-(void)bf_userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message;
@end
@interface BFWKDelegateController : UIViewController <WKScriptMessageHandler>
@property (weak , nonatomic) id<BFWKDelegate> 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) {
//網(wǎng)頁配置文件
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc]init];
//允許與網(wǎng)頁交互
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 - 中轉(zhuǎn)的Delegate
- (void)bf_userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
NSLog(@"name:%@ body:%@ frameInfo:%@",message.name,message.body,message.frameInfo);
}
在運行一下块蚌,當(dāng)前頁面銷毀的時候可以執(zhí)行到dealloc代碼啦
注意點:
1闰非、addScriptMessageHandler
要和removeScriptMessageHandlerForName
配套出現(xiàn),否則會造成內(nèi)存泄漏匈子。
2河胎、h5只能傳一個參數(shù),如果需要多個參數(shù)就需要用字典或者json組裝虎敦。
方案2:通過攔截WKNavigationDelegate代理
通過攔截WKNavigationDelegate的代理方法游岳,然后匹配目標(biāo)字符串
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(**void** (^)(WKNavigationActionPolicy))decisionHandler;
具體實例代碼如下:通過匹配是否是以js_native://alert
開頭的字符串
//在發(fā)送請求之前,決定是否跳轉(zhuǎn)
- (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];
//不允許跳轉(zhuǎn)
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
//允許跳轉(zhuǎn)
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上下文(運行環(huán)境)其徙,可用對象方法去執(zhí)行JS代碼(evaluateScript)胚迫,可通過上下文對象去獲取JS里的數(shù)據(jù)(上下文對象[key]),并用JSValue對象接收
- JSValue : 用于接收JSContext對象獲取的數(shù)據(jù)唾那,可以是任意對象访锻,方法。
實例代碼如下:
JSContext *jsContent = [[JSContext alloc]init];
jsContent[@"add"] = ^(int a, int b){
NSLog(@"a+b = %d",a + b);
};
[jsContent evaluateScript:@"add(20,30)"];
注意:js代碼要先被執(zhí)行,才能通過上下文獲取
說明:JSValue對其對應(yīng)的JS值和其所屬的JSContext對象都是強引用的關(guān)系期犬。因為JSValue需要這兩個東西來執(zhí)行JS代碼河哑,所以JSValue會一直持有著它們。
所以龟虎,在用block時璃谨,需考慮循環(huán)引用問題
- 不要在Block中直接使用JSValue:建議把JSValue當(dāng)做參數(shù)傳到Block中,而不是直接在Block內(nèi)部使用鲤妥,這樣Block就不會強引用JSValue了
- 不要在Block中直接使用JSContext:可以使用[JSContext currentContext] 方法來獲取當(dāng)前的Context
1佳吞、block方式:
用block定義js函數(shù),并執(zhí)行(OC調(diào)用執(zhí)行js棉安,而js是調(diào)用的oc block)
- (void)jsCallOCBlock
{
JSContext *ctx = [[JSContext alloc] init];
//OC中NSBlock對應(yīng)js中Function object
ctx[@"goto"] = ^(NSString *parmStr){
//block內(nèi)不要直接使用ctx底扳,會循環(huán)引用(ctx已經(jīng)引用block),若外部有JSValue贡耽,也不能在block內(nèi)直接調(diào)用(JSValue強持有了ctx )
//獲取JS調(diào)用參數(shù)
NSLog(@"parmStr:%@",parmStr);
//可以直接獲取所有參數(shù)
NSArray *arguments = [JSContext currentArguments];
NSLog(@"%@",arguments[0]);
};
//JS執(zhí)行代碼,調(diào)用goto方法衷模,并傳入?yún)?shù)school
NSString *jsCode = @"goto('school')";
//執(zhí)行
[ctx evaluateScript:jsCode];
}
2、JSExport 協(xié)議
需要在JS中生成OC對應(yīng)的類菇爪,然后再通過JS調(diào)用算芯。
方法:
通過自定義一個遵循JSExport的協(xié)議柒昏,把需要被JS訪問的OC類中的屬性凳宙,方法暴露給JS使用
步驟:
1、自定義協(xié)議职祷,協(xié)議遵循JSExport的協(xié)議氏涩,協(xié)議中的屬性和方法就是OC對象要暴露給JS,讓JS可以直接調(diào)用
#import <Foundation/Foundation.h>
#import <JavaScriptCore/JavaScriptCore.h>
@protocol PersonJSExport <JSExport>
@property (nonatomic, strong) NSString *address;
//無參數(shù)方法
- (void)play;
//因為JS函數(shù)命名規(guī)則和OC規(guī)則不一樣有梆,所以當(dāng)有多個參數(shù)時是尖,可以使用OC提供了一個宏*JSExportAs*,指定JS應(yīng)該生成什么樣的函數(shù)來對應(yīng)OC的方法泥耀。
//不使用JSExportAs指定關(guān)聯(lián)也可以正常調(diào)用饺汹,后面直接接多個參數(shù)
//多參數(shù)方法
//不指定關(guān)聯(lián)
- (void)play:(NSString *)address time:(NSString *)time;
//指定關(guān)聯(lián)
JSExportAs(gotoSchool, - (void)goToSchoolWithSchoolName:(NSString *)name address:(NSString *)address);
@end
2、自定義一個遵循第一步創(chuàng)建的協(xié)議的OC對象痰催,實現(xiàn)協(xié)議的方法
例如:Person對象
3兜辞、JS調(diào)用對象協(xié)議聲明的方法
- (void)jsCallOCClass
{
// 創(chuàng)建Person對象,Person對象必須遵守JSExport協(xié)議
Person *p = [[Person alloc] init];
p.address = @"aaa";
JSContext *ctx = [[JSContext alloc] init];
// 在JS中生成Person對象person,并且擁有p內(nèi)部的值
ctx[@"person"] = p;
// 執(zhí)行JS代碼
//NSString *jsCode = @"person.play()";
NSString *jsCode = @"person.play('北京天安門','now')";
//NSString *jsCode = @"person.gotoSchool('實驗中學(xué)','廣州')";
[ctx evaluateScript:jsCode];
}
另外夸溶,若要調(diào)用OC系統(tǒng)的類逸吵,例如UIView
需要同樣創(chuàng)建協(xié)議缝裁,只是在第三步用runtime給系統(tǒng)類UIView添加創(chuàng)建的協(xié)議
class_addProtocol([UIView class], @protocol(UIViewlJSExport));
方案4: 三方庫 WebViewJavaScripteBridge
OC 調(diào)取JS
方案1: WKWebView直接執(zhí)行 evaluateJavaScript 方法
//原生回調(diào)JS
[self.webView evaluateJavaScript:@"nativeCallbackJscMothod('%@')" completionHandler:^(**id** **_Nullable** s, NSError * **_Nullable** error) {
NSLog(@"執(zhí)行完成啦");
}];
方案2:蘋果原生API:JavaScriptCore (iOS7.0+ 使用)
oc獲取js變量,注:js代碼要先被執(zhí)行韩脑,才能通過上下文獲取
- (void)ocGetJSVar
{
//定義JS代碼
NSString *jsCode = @"var a = 'a'";
//創(chuàng)建JS運行環(huán)境
JSContext *ctx = [[JSContext alloc] init];
//!!!:執(zhí)行JS代碼---先執(zhí)行,后面才能獲取
[ctx evaluateScript:jsCode];
//獲取變量
JSValue *value = ctx[**@“a"**];
//JSValue轉(zhuǎn)NSString
NSString *valueStr = value.toString;
//打印結(jié)果:a
NSLog(@"%@",valueStr);
}
oc調(diào)用js方法允懂,并獲取返回結(jié)果
- (void)ocCallJSFunc
{
NSString *jsCode = @"function say(str){"
" return str; "
"}";
// 創(chuàng)建JS運行環(huán)境
JSContext *ctx = [[JSContext alloc] init];
// 執(zhí)行JS代碼
[ctx evaluateScript:jsCode];
//!!!:執(zhí)行JS代碼---先執(zhí)行衩匣,后面才能獲取
JSValue *say = ctx[@"say"];
// OC調(diào)用JS方法,獲取方法返回值
JSValue *result = [say callWithArguments:@[@"hello world!"]];
// 打印結(jié)果:hello world!
NSLog(@"%@",result);
}