@xifenglang-33250
2017-01-06T16:49:27.000000Z
字数 12714
阅读 6265
最近公司开发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];// 再加载URLself.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){/// -999return;}}
解决方案是在执行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;}}