@xifenglang-33250
2017-01-07T00:49:27.000000Z
字数 12714
阅读 5906
最近公司开发Web APP,绝大部分界面都是Web网页,所以用WKWebView加载网页,由于Web端的兄弟也是半桶水,结果和他踩了不少坑。在此对WKWebView的使用做些小结,填些踩过的坑。
对WKWebView就不细说了,有兴趣可以看看末尾的参考文献
/// 偏好设置,涉及JS交互
WKWebViewConfiguration * configuration = [[WKWebViewConfiguration alloc] init];
configuration.preferences = [[WKPreferences alloc]init];
configuration.preferences.javaScriptEnabled = YES;
configuration.preferences.javaScriptCanOpenWindowsAutomatically = NO;
configuration.processPool = [[WKProcessPool alloc]init];
configuration.allowsInlineMediaPlayback = YES;
// if (iOS9()) {
// /// 缓存机制(未研究)
// configuration.websiteDataStore = [WKWebsiteDataStore defaultDataStore];
// }
configuration.userContentController = [[WKUserContentController alloc] init];
WKWebView * webView = [[WKWebView alloc]initWithFrame:JKMainScreen configuration:configuration];
/// 侧滑返回上一页,侧滑返回不会加载新的数据,选择性开启
self.webView.allowsBackForwardNavigationGestures = YES;
/// 在这个代理相应的协议方法可以监听加载网页的周期和结果
self.webView.navigationDelegate = self;
/// 这个代理对应的协议方法常用来显示弹窗
self.webView.UIDelegate = self;
/// 如果涉及到JS交互,比如Web通过JS调iOS native,最好在[webView loadRequest:]前注入JS对象,详细代码见文章后半部分代码。
self.jsBridge = [[JSBridge alloc]initWithUserContentController:configuration.userContentController];
self.jsBridge.webView = webView;
self.jsBridge.webViewController = self;
KVO能监听加载进度,也能监听当前Url的Title。
UIProgressView *progressView = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault];
[webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"estimatedProgress"]) {
if (object == self.webView) {
if (self.webView.estimatedProgress == 1.0) {
self.progressView.progress = 1.0;
[UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
self.progressView.alpha = 0.0f;
} completion:nil];
} else {
self.progressView.progress = self.webView.estimatedProgress;
}
}
}
- (void)dealloc{
[self.webView removeObserver:self forKeyPath:@"estimatedProgress"];
}
}
/// 发送请求前决定是否跳转,并在此拦截拨打电话的URL
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
/// decisionHandler(WKNavigationActionPolicyCancel);不允许加载
/// decisionHandler(WKNavigationActionPolicyAllow);允许加载
decisionHandler(WKNavigationActionPolicyAllow);
}
/// 收到响应后决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{
decisionHandler(WKNavigationResponsePolicyAllow);
}
/// 内容开始加载
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation{
self.progressView.alpha = 1.0;
}
/// 加载完成
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation{
[self hideErrorView];
if (self.progressView.progress < 1.0) {
[UIView animateWithDuration:0.1 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
self.progressView.alpha = 0.0f;
} completion:nil];
}
/// 禁止长按弹窗,UIActionSheet样式弹窗
[webView evaluateJavaScript:@"document.documentElement.style.webkitTouchCallout='none';" completionHandler:nil];
/// 禁止长按弹窗,UIMenuController样式弹窗(效果不佳)
[webView evaluateJavaScript:@"document.documentElement.style.webkitUserSelect='none';" completionHandler:nil];
}
/// 加载失败
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error{
if (error.code == NSURLErrorNotConnectedToInternet) {
[self showErrorView];
/// 无网络(APP第一次启动并且没有得到网络授权时可能也会报错)
} else if (error.code == NSURLErrorCancelled){
/// -999 上一页面还没加载完,就加载当下一页面,就会报这个错。
return;
}
JKLog(@"webView加载失败:error %@",error);
}
// 在JS端调用alert函数时(警告弹窗),会触发此代理方法。
// 通过completionHandler()回调JS
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{
JKAlertManager * manager = [JKAlertManager alertWithPreferredStyle:UIAlertControllerStyleAlert title:@"提示" message:message];
[manager configueCancelTitle:nil destructiveIndex:JKAlertDestructiveIndexNone otherTitle:@"确定", nil];
[manager showAlertFromController:self actionBlock:^(JKAlertManager *tempAlertManager, NSInteger actionIndex, NSString *actionTitle) {
if (actionIndex != tempAlertManager.cancelIndex) {
completionHandler();
}
}];
}
// JS端调用confirm函数时(确认、取消式弹窗),会触发此方法
// completionHandler(true)返回结果
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{
JKAlertManager * manager = [JKAlertManager alertWithPreferredStyle:UIAlertControllerStyleAlert title:@"提示" message:message];
[manager configueCancelTitle:@"取消" destructiveIndex:JKAlertDestructiveIndexNone otherTitle:@"确定", nil];
[manager showAlertFromController:self actionBlock:^(JKAlertManager *tempAlertManager, NSInteger actionIndex, NSString *actionTitle) {
if (actionIndex != tempAlertManager.cancelIndex) {
completionHandler(YES);
}else{
completionHandler(NO);
}
}];
}
/// JS调用prompt函数(输入框)时回调,completionHandler回调结果
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable))completionHandler{
JKAlertManager * manager = [JKAlertManager alertWithPreferredStyle:UIAlertControllerStyleAlert title:@"提示" message:prompt];
[manager configueCancelTitle:@"取消" destructiveIndex:JKAlertDestructiveIndexNone otherTitle:@"确定", nil];
[manager addTextFieldWithPlaceholder:defaultText secureTextEntry:NO ConfigurationHandler:^(UITextField * _Nonnull textField) {
} textFieldTextChanged:^(UITextField * _Nullable textField) {
}];
[manager showAlertFromController:self actionBlock:^(JKAlertManager * _Nullable tempAlertManager, NSInteger actionIndex, NSString * _Nullable actionTitle) {
completionHandler(tempAlertManager.textFields.firstObject.text);
}];
}
如果用WKWebView,JS调iOS端必须使用window.webkit.messageHandlers.kJS_Name.postMessage(null)
,kJS_Name是iOS端提供的JS交互name,在注入JS交互Handler时用到:[userContentController addScriptMessageHandler:self name:kJS_Name]
下面有个HTML端的iOSCallJsAlert函数,里面会执行alert弹窗,并通过JS调iOS端(kJS_Name)
function iOSCallJsAlert() {
alert('弹个窗,再调用iOS端的kJS_Name');
window.webkit.messageHandlers.kJS_Name.postMessage({body: 'paramters'});
}
咱要实现在iOS端通过JS调用这个iOSCallJsAlert函数,并接受JS调iOS端的ScriptMessage。有以下主要代码:
首先添加JS交互的消息处理者(遵守WKScriptMessageHandler协议)以及JS_Name(一般由iOS端提供给Web端)。
[WKUserContentController addScriptMessageHandler:JS_ScriptMessageReceiver name:JS_Name]
有添加就有移除,一般在ViewDidDisappear中移除,不然JS_ScriptMessageReceiver会被强引用而无法释放(内存泄露),个人猜测是被WebKit里面某个单例强引用。
[userContentController removeScriptMessageHandlerForName:JS_Name]
实现WKScriptMessageHandler协议方法,用来接收JS调iOS的消息。
WKScriptMessage.name即[WKUserContentController addScriptMessageHandler:JS_ScriptMessageReceiver name:JS_Name]
中的JS_Name,可以区分不同的JS交互,message.body是传递的参数。
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
JKLog(@"JS调iOS name : %@ body : %@",message.name,message.body);
}
iOS端通过JS中的函数就简单多了,调用一个方法即可。
@"iOSCallJsAlert()"代表要调用的函数名,如果有参数就这样写@"iOSCallJsAlert('p1','p2')"
[webView evaluateJavaScript:@"iOSCallJsAlert()" completionHandler:nil]
我之前是看了标哥的文章,讲的很细,不过现在找不到原文了,就找了个转载的文章,详见参考文献
[userContentController removeScriptMessageHandlerForName:JS_Name]
WebViewController.m
/// JSBridge是封装的JS交互桥梁,遵守WKScriptMessageHandler协议
- (void)reloadWebViewWithUrl:(NSString *)url{
// 先移除
[self.jsBridge removeAllUserScripts];
// 再注入
self.jsBridge.userScriptNames = @[kJS_Login,kJS_Logout,kJS_Alipay,kJS_WeiChatPay,kJS_Location];
// 再加载URL
self.urlStr = url;
[self.webView loadRequest:[[NSURLRequest alloc] initWithURL:self.urlStr.URL]];
}
- (void)viewDidDisappear:(BOOL)animated{
[super viewDidDisappear:animated];
/// 移除,避免JS_ScriptMessageReceiver被引用
[self.jsBridge removeAllUserScripts];
}
JSBridge.m 实现WKScriptMessageHandler协议方法
@interface JSBridge ()<WKScriptMessageHandler>
@property (nonatomic, weak)WKUserContentController * userContentController;
@end
- (instancetype)initWithUserContentController:(WKUserContentController *)userContentController{
if (self = [super init]) {
_userContentController = userContentController;
}return self;
}
/// 注入JS MessageHandler和Name
- (void)setUserScriptNames:(NSArray *)userScriptNames{
_userScriptNames = userScriptNames;
[userScriptNames enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[self.userContentController addScriptMessageHandler:self name:obj];
}];
}
/// 移除JS MessageHandler
- (void)removeAllUserScripts{
[self.userScriptNames enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[self.userContentController removeScriptMessageHandlerForName:obj];
}];
self.userScriptNames = nil;
}
/// 接收JS调iOS的事件消息
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
JKLog(@"JS调iOS name : %@ body : %@",message.name,message.body);
if ([message.name isEqualToString:kJS_Login]) {
/// 登录JS
} else if ([message.name isEqualToString:kJS_Logout]) {
/// 退出JS
}
}
@end
-(WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {
if (!navigationAction.targetFrame.isMainFrame) {
[webView loadRequest:navigationAction.request];
}
return nil;
}
<a href="tel:123456789">拨号</a>
调iOS拨打电话的功能,需要我们在WKNavigationDelegate协议方法中截取URL中的号码再拨打电话。
/// 发送请求前决定是否跳转,并在此拦截拨打电话的URL
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
/// <a href="tel:123456789">拨号</a>
if ([navigationAction.request.URL.scheme isEqualToString:@"tel"]) {
decisionHandler(WKNavigationActionPolicyCancel);
NSString * mutStr = [NSString stringWithFormat:@"telprompt://%@",navigationAction.request.URL.resourceSpecifier];
if ([[UIApplication sharedApplication] canOpenURL:mutStr.URL]) {
if (iOS10()) {
[[UIApplication sharedApplication] openURL:mutStr.URL options:@{} completionHandler:^(BOOL success) {}];
} else {
[[UIApplication sharedApplication] openURL:mutStr.URL];
}
}
} else {
decisionHandler(WKNavigationActionPolicyAllow);
}
}
goBack
或reload
或goToBackForwardListItem
后马上执行loadRequest
,即一起执行,在didFailProvisionalNavigation方法中会报错,error.code = -999( NSURLErrorCancelled)。
[self.webView goBack];
[self.webView loadRequest:[[NSURLRequest alloc] initWithURL:URL]];
原因是上一页面还没加载完,就加载当下一页面,会报-999错误。
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error{
if (error.code == NSURLErrorCancelled){
/// -999
return;
}
}
解决方案是在执行goBack
或reload
或goToBackForwardListItem
后延迟一会儿(0.5秒)再执行loadRequest
。
[self.webView goBack];
/// 延迟加载新的url,否则报错-999
[self excuteDelayTask:0.5 InMainQueue:^{
[self.webView loadRequest:[[NSURLRequest alloc] initWithURL:URL]];
}];
self.webView.allowsBackForwardNavigationGestures = YES
; WKWebView侧滑返回会直接加载之前缓存下来的数据(也有说是缓存了渲染),不会刷新界面,而有时需要在返回后刷新数据,就需要做特殊处理。
- (void)reloadWebViewWithUrl:(NSString *)url backToHomePage:(BOOL)backToHomePage{
void (^LoadWebViewBlock)() = ^() {
/// 每次加载新url前重新注入JS对象
[self.jsBridge removeAllUserScripts];
self.jsBridge.userScriptNames = @[kJS_Login,kJS_Logout,kJS_Alipay,kJS_WeiChatPay,kJS_Location];
self.urlStr = url;
[self.webView loadRequest:[[NSURLRequest alloc] initWithURL:self.urlStr.URL]];
};
if (self.webView.backForwardList.backList.count && backToHomePage) {
/// 返回首页再跳转,并且保留WKNavigation对象
self.gobackNavigation = [self.webView goToBackForwardListItem:self.webView.backForwardList.backList.firstObject];
/// 延迟加载新的url,否则报错-999
[self excuteDelayTask:0.5 InMainQueue:^{
LoadWebViewBlock();
}];
} else {
LoadWebViewBlock();
}
}
/// 根据self.gobackNavigation重载页面
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation{
/// 之前的代码已省略
/// 新增下面的代码
if ([navigation isEqual:self.gobackNavigation] || !navigation) {
/// 重载刷新
[self.webView reload];
self.gobackNavigation = nil;
}
}