[关闭]
@xifenglang-33250 2017-01-07T00:49:27.000000Z 字数 12714 阅读 5828

WKWebView使用小结

最近公司开发Web APP,绝大部分界面都是Web网页,所以用WKWebView加载网页,由于Web端的兄弟也是半桶水,结果和他踩了不少坑。在此对WKWebView的使用做些小结,填些踩过的坑。

目录


配置WKWebView [↑↑目录]

对WKWebView就不细说了,有兴趣可以看看末尾的参考文献

  1. /// 偏好设置,涉及JS交互
  2. WKWebViewConfiguration * configuration = [[WKWebViewConfiguration alloc] init];
  3. configuration.preferences = [[WKPreferences alloc]init];
  4. configuration.preferences.javaScriptEnabled = YES;
  5. configuration.preferences.javaScriptCanOpenWindowsAutomatically = NO;
  6. configuration.processPool = [[WKProcessPool alloc]init];
  7. configuration.allowsInlineMediaPlayback = YES;
  8. // if (iOS9()) {
  9. // /// 缓存机制(未研究)
  10. // configuration.websiteDataStore = [WKWebsiteDataStore defaultDataStore];
  11. // }
  12. configuration.userContentController = [[WKUserContentController alloc] init];
  13. WKWebView * webView = [[WKWebView alloc]initWithFrame:JKMainScreen configuration:configuration];
  14. /// 侧滑返回上一页,侧滑返回不会加载新的数据,选择性开启
  15. self.webView.allowsBackForwardNavigationGestures = YES;
  16. /// 在这个代理相应的协议方法可以监听加载网页的周期和结果
  17. self.webView.navigationDelegate = self;
  18. /// 这个代理对应的协议方法常用来显示弹窗
  19. self.webView.UIDelegate = self;
  20. /// 如果涉及到JS交互,比如Web通过JS调iOS native,最好在[webView loadRequest:]前注入JS对象,详细代码见文章后半部分代码。
  21. self.jsBridge = [[JSBridge alloc]initWithUserContentController:configuration.userContentController];
  22. self.jsBridge.webView = webView;
  23. self.jsBridge.webViewController = self;

利用KVO实现进度条 [↑↑目录]

KVO能监听加载进度,也能监听当前Url的Title。

  1. UIProgressView *progressView = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault];
  2. [webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil];
  3. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
  4. if ([keyPath isEqualToString:@"estimatedProgress"]) {
  5. if (object == self.webView) {
  6. if (self.webView.estimatedProgress == 1.0) {
  7. self.progressView.progress = 1.0;
  8. [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
  9. self.progressView.alpha = 0.0f;
  10. } completion:nil];
  11. } else {
  12. self.progressView.progress = self.webView.estimatedProgress;
  13. }
  14. }
  15. }
  16. - (void)dealloc{
  17. [self.webView removeObserver:self forKeyPath:@"estimatedProgress"];
  18. }
  19. }

WKNavigationDelegate协议,监听网页加载周期 [↑↑目录]

  1. /// 发送请求前决定是否跳转,并在此拦截拨打电话的URL
  2. - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
  3. /// decisionHandler(WKNavigationActionPolicyCancel);不允许加载
  4. /// decisionHandler(WKNavigationActionPolicyAllow);允许加载
  5. decisionHandler(WKNavigationActionPolicyAllow);
  6. }
  7. /// 收到响应后决定是否跳转
  8. - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{
  9. decisionHandler(WKNavigationResponsePolicyAllow);
  10. }
  11. /// 内容开始加载
  12. - (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation{
  13. self.progressView.alpha = 1.0;
  14. }
  15. /// 加载完成
  16. - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation{
  17. [self hideErrorView];
  18. if (self.progressView.progress < 1.0) {
  19. [UIView animateWithDuration:0.1 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
  20. self.progressView.alpha = 0.0f;
  21. } completion:nil];
  22. }
  23. /// 禁止长按弹窗,UIActionSheet样式弹窗
  24. [webView evaluateJavaScript:@"document.documentElement.style.webkitTouchCallout='none';" completionHandler:nil];
  25. /// 禁止长按弹窗,UIMenuController样式弹窗(效果不佳)
  26. [webView evaluateJavaScript:@"document.documentElement.style.webkitUserSelect='none';" completionHandler:nil];
  27. }
  28. /// 加载失败
  29. - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error{
  30. if (error.code == NSURLErrorNotConnectedToInternet) {
  31. [self showErrorView];
  32. /// 无网络(APP第一次启动并且没有得到网络授权时可能也会报错)
  33. } else if (error.code == NSURLErrorCancelled){
  34. /// -999 上一页面还没加载完,就加载当下一页面,就会报这个错。
  35. return;
  36. }
  37. JKLog(@"webView加载失败:error %@",error);
  38. }

WKUIDelegate协议,常用来显示UIAlertController弹窗 [↑↑目录]

  1. // 在JS端调用alert函数时(警告弹窗),会触发此代理方法。
  2. // 通过completionHandler()回调JS
  3. - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{
  4. JKAlertManager * manager = [JKAlertManager alertWithPreferredStyle:UIAlertControllerStyleAlert title:@"提示" message:message];
  5. [manager configueCancelTitle:nil destructiveIndex:JKAlertDestructiveIndexNone otherTitle:@"确定", nil];
  6. [manager showAlertFromController:self actionBlock:^(JKAlertManager *tempAlertManager, NSInteger actionIndex, NSString *actionTitle) {
  7. if (actionIndex != tempAlertManager.cancelIndex) {
  8. completionHandler();
  9. }
  10. }];
  11. }
  12. // JS端调用confirm函数时(确认、取消式弹窗),会触发此方法
  13. // completionHandler(true)返回结果
  14. - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{
  15. JKAlertManager * manager = [JKAlertManager alertWithPreferredStyle:UIAlertControllerStyleAlert title:@"提示" message:message];
  16. [manager configueCancelTitle:@"取消" destructiveIndex:JKAlertDestructiveIndexNone otherTitle:@"确定", nil];
  17. [manager showAlertFromController:self actionBlock:^(JKAlertManager *tempAlertManager, NSInteger actionIndex, NSString *actionTitle) {
  18. if (actionIndex != tempAlertManager.cancelIndex) {
  19. completionHandler(YES);
  20. }else{
  21. completionHandler(NO);
  22. }
  23. }];
  24. }
  25. /// JS调用prompt函数(输入框)时回调,completionHandler回调结果
  26. - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable))completionHandler{
  27. JKAlertManager * manager = [JKAlertManager alertWithPreferredStyle:UIAlertControllerStyleAlert title:@"提示" message:prompt];
  28. [manager configueCancelTitle:@"取消" destructiveIndex:JKAlertDestructiveIndexNone otherTitle:@"确定", nil];
  29. [manager addTextFieldWithPlaceholder:defaultText secureTextEntry:NO ConfigurationHandler:^(UITextField * _Nonnull textField) {
  30. } textFieldTextChanged:^(UITextField * _Nullable textField) {
  31. }];
  32. [manager showAlertFromController:self actionBlock:^(JKAlertManager * _Nullable tempAlertManager, NSInteger actionIndex, NSString * _Nullable actionTitle) {
  33. completionHandler(tempAlertManager.textFields.firstObject.text);
  34. }];
  35. }

JS交互实现流程 [↑↑目录]

如果用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)

  1. function iOSCallJsAlert() {
  2. alert('弹个窗,再调用iOS端的kJS_Name');
  3. window.webkit.messageHandlers.kJS_Name.postMessage({body: 'paramters'});
  4. }

咱要实现在iOS端通过JS调用这个iOSCallJsAlert函数,并接受JS调iOS端的ScriptMessage。有以下主要代码:

首先添加JS交互的消息处理者(遵守WKScriptMessageHandler协议)以及JS_Name(一般由iOS端提供给Web端)。

  1. [WKUserContentController addScriptMessageHandler:JS_ScriptMessageReceiver name:JS_Name]

有添加就有移除,一般在ViewDidDisappear中移除,不然JS_ScriptMessageReceiver会被强引用而无法释放(内存泄露),个人猜测是被WebKit里面某个单例强引用。

  1. [userContentController removeScriptMessageHandlerForName:JS_Name]

实现WKScriptMessageHandler协议方法,用来接收JS调iOS的消息。
WKScriptMessage.name即[WKUserContentController addScriptMessageHandler:JS_ScriptMessageReceiver name:JS_Name]中的JS_Name,可以区分不同的JS交互,message.body是传递的参数。

  1. - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
  2. JKLog(@"JS调iOS name : %@ body : %@",message.name,message.body);
  3. }

iOS端通过JS中的函数就简单多了,调用一个方法即可。
@"iOSCallJsAlert()"代表要调用的函数名,如果有参数就这样写@"iOSCallJsAlert('p1','p2')"

  1. [webView evaluateJavaScript:@"iOSCallJsAlert()" completionHandler:nil]

我之前是看了标哥的文章,讲的很细,不过现在找不到原文了,就找了个转载的文章,详见参考文献


JS交互 踩坑、填坑 [↑↑目录]


  1. [userContentController removeScriptMessageHandlerForName:JS_Name]

WebViewController.m

  1. /// JSBridge是封装的JS交互桥梁,遵守WKScriptMessageHandler协议
  2. - (void)reloadWebViewWithUrl:(NSString *)url{
  3. // 先移除
  4. [self.jsBridge removeAllUserScripts];
  5. // 再注入
  6. self.jsBridge.userScriptNames = @[kJS_Login,kJS_Logout,kJS_Alipay,kJS_WeiChatPay,kJS_Location];
  7. // 再加载URL
  8. self.urlStr = url;
  9. [self.webView loadRequest:[[NSURLRequest alloc] initWithURL:self.urlStr.URL]];
  10. }
  11. - (void)viewDidDisappear:(BOOL)animated{
  12. [super viewDidDisappear:animated];
  13. /// 移除,避免JS_ScriptMessageReceiver被引用
  14. [self.jsBridge removeAllUserScripts];
  15. }

JSBridge.m 实现WKScriptMessageHandler协议方法

  1. @interface JSBridge ()<WKScriptMessageHandler>
  2. @property (nonatomic, weak)WKUserContentController * userContentController;
  3. @end
  4. - (instancetype)initWithUserContentController:(WKUserContentController *)userContentController{
  5. if (self = [super init]) {
  6. _userContentController = userContentController;
  7. }return self;
  8. }
  9. /// 注入JS MessageHandler和Name
  10. - (void)setUserScriptNames:(NSArray *)userScriptNames{
  11. _userScriptNames = userScriptNames;
  12. [userScriptNames enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
  13. [self.userContentController addScriptMessageHandler:self name:obj];
  14. }];
  15. }
  16. /// 移除JS MessageHandler
  17. - (void)removeAllUserScripts{
  18. [self.userScriptNames enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
  19. [self.userContentController removeScriptMessageHandlerForName:obj];
  20. }];
  21. self.userScriptNames = nil;
  22. }
  23. /// 接收JS调iOS的事件消息
  24. - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
  25. JKLog(@"JS调iOS name : %@ body : %@",message.name,message.body);
  26. if ([message.name isEqualToString:kJS_Login]) {
  27. /// 登录JS
  28. } else if ([message.name isEqualToString:kJS_Logout]) {
  29. /// 退出JS
  30. }
  31. }
  32. @end


  1. -(WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {
  2. if (!navigationAction.targetFrame.isMainFrame) {
  3. [webView loadRequest:navigationAction.request];
  4. }
  5. return nil;
  6. }

  1. /// 发送请求前决定是否跳转,并在此拦截拨打电话的URL
  2. - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
  3. /// <a href="tel:123456789">拨号</a>
  4. if ([navigationAction.request.URL.scheme isEqualToString:@"tel"]) {
  5. decisionHandler(WKNavigationActionPolicyCancel);
  6. NSString * mutStr = [NSString stringWithFormat:@"telprompt://%@",navigationAction.request.URL.resourceSpecifier];
  7. if ([[UIApplication sharedApplication] canOpenURL:mutStr.URL]) {
  8. if (iOS10()) {
  9. [[UIApplication sharedApplication] openURL:mutStr.URL options:@{} completionHandler:^(BOOL success) {}];
  10. } else {
  11. [[UIApplication sharedApplication] openURL:mutStr.URL];
  12. }
  13. }
  14. } else {
  15. decisionHandler(WKNavigationActionPolicyAllow);
  16. }
  17. }

  1. [self.webView goBack];
  2. [self.webView loadRequest:[[NSURLRequest alloc] initWithURL:URL]];

原因是上一页面还没加载完,就加载当下一页面,会报-999错误。

  1. - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error{
  2. if (error.code == NSURLErrorCancelled){
  3. /// -999
  4. return;
  5. }
  6. }

解决方案是在执行goBackreloadgoToBackForwardListItem后延迟一会儿(0.5秒)再执行loadRequest

  1. [self.webView goBack];
  2. /// 延迟加载新的url,否则报错-999
  3. [self excuteDelayTask:0.5 InMainQueue:^{
  4. [self.webView loadRequest:[[NSURLRequest alloc] initWithURL:URL]];
  5. }];

  1. - (void)reloadWebViewWithUrl:(NSString *)url backToHomePage:(BOOL)backToHomePage{
  2. void (^LoadWebViewBlock)() = ^() {
  3. /// 每次加载新url前重新注入JS对象
  4. [self.jsBridge removeAllUserScripts];
  5. self.jsBridge.userScriptNames = @[kJS_Login,kJS_Logout,kJS_Alipay,kJS_WeiChatPay,kJS_Location];
  6. self.urlStr = url;
  7. [self.webView loadRequest:[[NSURLRequest alloc] initWithURL:self.urlStr.URL]];
  8. };
  9. if (self.webView.backForwardList.backList.count && backToHomePage) {
  10. /// 返回首页再跳转,并且保留WKNavigation对象
  11. self.gobackNavigation = [self.webView goToBackForwardListItem:self.webView.backForwardList.backList.firstObject];
  12. /// 延迟加载新的url,否则报错-999
  13. [self excuteDelayTask:0.5 InMainQueue:^{
  14. LoadWebViewBlock();
  15. }];
  16. } else {
  17. LoadWebViewBlock();
  18. }
  19. }
  20. /// 根据self.gobackNavigation重载页面
  21. - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation{
  22. /// 之前的代码已省略
  23. /// 新增下面的代码
  24. if ([navigation isEqual:self.gobackNavigation] || !navigation) {
  25. /// 重载刷新
  26. [self.webView reload];
  27. self.gobackNavigation = nil;
  28. }
  29. }

参考文献

[↑↑目录]

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注