WKWebView的使用

原文地址:https://www.jianshu.com/p/5cf0d241ae12
原文Demo地址:https://github.com/wslcmk/WKWebView
本文是在我的理解的基础上加以梳理学习,如有侵权请联系删除.

一.WKWebView的基本使用

#import  
//初始化
_webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT) configuration:config];
// UI代理
_webView.UIDelegate = self;
// 导航代理
_webView.navigationDelegate = self;
// 是否允许手势左滑返回上一级, 类似导航控制的左滑返回
_webView.allowsBackForwardNavigationGestures = YES;
//可返回的页面列表, 存储已打开过的网页 
WKBackForwardList * backForwardList = [_webView backForwardList];

//1.加载URL
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.chinadaily.com.cn"]];
[request addValue:[self readCurrentCookieWithDomain:@"http://www.chinadaily.com.cn"] forHTTPHeaderField:@"Cookie"];
[_webView loadRequest:request];

//2.加载本地html文件
NSString *path = [[NSBundle mainBundle] pathForResource:@"JStoOC.html" ofType:nil];
NSString *htmlString = [[NSString alloc]initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
[_webView loadHTMLString:htmlString baseURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]]];   

二.WKWebView的相关类

关于WKWebView各种类的详细属性介绍请参考WKWebView相关API详解

  • WKWebViewConfiguration
    ① 使用WKWebViewConfiguration类,你可以决定网页的渲染时机,媒体的播放方式,用户选择项目的粒度,以及很多其他的选项。
    ② WKWebViewConfiguration只会在webview第一次初始化的时候使用,你不能用此类来改变一个已经初始化完成的webview的配置。
  //创建网页配置对象
  WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
  // 是使用h5的视频播放器在线播放, 还是使用原生播放器全屏播放
  config.allowsInlineMediaPlayback = YES;
  //设置视频是否需要用户手动播放  设置为NO则会允许自动播放
  config.requiresUserActionForMediaPlayback = YES;
  //设置是否允许画中画技术 在特定设备上有效
  config.allowsPictureInPictureMediaPlayback = YES;
  //设置请求的User-Agent信息中应用程序名称 iOS9后可用
  config.applicationNameForUserAgent = @"ChinaDailyForiPad";
  • WKPreferences
    ① WKUserScript一个WKPreferences封装了一个webview的偏好选项
    ② 在WKWebViewConfiguration中设置
  // 创建设置对象
  WKPreferences *preference = [[WKPreferences alloc]init];
  //最小字体大小 当将javaScriptEnabled属性设置为NO时,可以看到明显的效果
  preference.minimumFontSize = 0;
  //设置是否支持javaScript 默认是支持的
  preference.javaScriptEnabled = YES;
  // 在iOS上默认为NO,表示是否允许不经过用户交互由javaScript自动打开窗口
  preference.javaScriptCanOpenWindowsAutomatically = YES;
  config.preferences = preference;
  • WKUserScript
    一个WKUserScript对象代表了一个可以被注入网页中的脚本

  • WKScriptMessageHandler
    ① 为一个实现了WKScriptMessageHandler协议的类提供一个接收来自网页的JavaScript调用的方法
    ② 当收到来自网页的一次JavaScipt调用时响应 , 从而实现JS调用原生 , 添加给WKUserContentController

// 在当前VC里面(遵守WKScriptMessageHandler协议的类)必须要实现下面的方法
// 通过接收JS传出消息的name进行捕捉实现JS调用原生
// userContentController是调用该方法的WKUserContentController,message是收到的JavaScript消息
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    NSLog(@"name:%@\\\\n body:%@\\\\n frameInfo:%@\\\\n",message.name,message.body,message.frameInfo);
    //用message.body获得JS传出的参数体
    NSDictionary * parameter = message.body;
    if([message.name isEqualToString:@"jsToOcNoPrams"]){
     //JS调用OC不带参数
    }else if([message.name isEqualToString:@"jsToOcWithPrams"]){
     //JS调用OC带参数
    }
}
  • WKUserContentController
    ① WKUserContentController对象提供了一种向WebView注册JavaScript消息或者注入JavaScript脚本的方法
    ② 在WKWebViewConfiguration中设置
   //自定义的WKScriptMessageHandler 是为了解决内存不释放的问题
   WeakWebViewScriptMessageDelegate *weakScriptMessageDelegate =[[WeakWebViewScriptMessageDelegate alloc] initWithDelegate:self];
   //这个类主要用来做native与JavaScript的交互管理
   WKUserContentController * wkUController = [[WKUserContentController alloc] init];

   // ① 注册一个name为jsToOcNoPrams的js方法 (最后会调用上一个代码块的方法)
   [wkUController addScriptMessageHandler:weakScriptMessageDelegate  name:@"jsToOcNoPrams"];
   [wkUController addScriptMessageHandler:weakScriptMessageDelegate  name:@"jsToOcWithPrams"];

   // ② 用于进行JavaScript注入
   //以下代码适配文本大小,由UIWebView换为WKWebView后,会发现字体小了很多,这应该是WKWebView与html的兼容问题,解决办法是修改原网页,要么我们手动注入JS
   NSString *jSString = @"var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);";
   //创建WKUserScript并注入
   WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jSString injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
   [wkUController addUserScript:wkUScript];

   config.userContentController = wkUController;

   //用完记得移除注册的js方法
   [[_webView configuration].userContentController removeScriptMessageHandlerForName:@"jsToOcNoPrams"];
   [[_webView configuration].userContentController removeScriptMessageHandlerForName:@"jsToOcWithPrams"];
  • 补充:自定义一个类遵守WKScriptMessageHandler协议,实现协议方法
 // WKWebView 解决内存不释放的问题
 @interface WeakWebViewScriptMessageDelegate : NSObject

 //WKScriptMessageHandler 这个协议类专门用来处理JavaScript调用原生OC的方法
 @property (nonatomic, weak) id scriptDelegate;
 - (instancetype)initWithDelegate:(id)scriptDelegate;
    
 @end

 @implementation WeakWebViewScriptMessageDelegate
    
 - (instancetype)initWithDelegate:(id)scriptDelegate {
    self = [super init];
    if (self) {
        _scriptDelegate = scriptDelegate;
    }
    return self;
 }
    
 #pragma mark - WKScriptMessageHandler
 //遵循WKScriptMessageHandler协议,必须实现如下方法,然后把方法向外传递
 //通过接收JS传出消息的name进行捕捉的回调方法
 - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
 //把消息传到当前VC里面
    if ([self.scriptDelegate respondsToSelector:@selector(userContentController:didReceiveScriptMessage:)]) {
        [self.scriptDelegate userContentController:userContentController didReceiveScriptMessage:message];
    }
 }
    
 @end

三.WKWebView的两个代理

  • WKNavigationDelegate
    通过实现WKNavigationDelegate协议的方法可以自定义WebView接受、加载和完成浏览请求过程的一些行为。
当WebView开始接收网页内容时触发
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation;

当WevView的内容开始加载时触发
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation;

主机地址重定向时触发
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation;

页面发生错误时触发
- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error;

当页面加载内容过程中发生错误时触发
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error;

当前页面加载完成后触发
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation;

当WebContent进程中止时触发
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView;

决定是否允许或者取消一次页面加载请求(类似UIWebView的shouldStartLoadWithRequest)
① WKNavigationAction描述了触发本次请求的行为
② decisionHandler block在app已经决定允许或取消页面请求时调用
③ 这个block携带一个参数,是WKNavigationActionPolicy枚举类型
④ WKNavigationActionPolicy枚举包括WKNavigationActionPolicyCancel取消和 WKNavigationActionPolicyAllow允许两个枚举值
⑤ 你可以立即调用该block或者保存block并在以后的时间异步调用它
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
NSString * urlStr = navigationAction.request.URL.absoluteString;
    NSLog(@"发送跳转请求:%@",urlStr);
    //自己定义的协议头
    NSString *htmlHeadString = @"github://";
    if([urlStr hasPrefix:htmlHeadString]){
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"通过截取URL调用OC" message:@"你想前往我的Github主页?" preferredStyle:UIAlertControllerStyleAlert];
        [alertController addAction:([UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {   
        }])];
        [alertController addAction:([UIAlertAction actionWithTitle:@"打开" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
            NSURL * url = [NSURL URLWithString:[urlStr stringByReplacingOccurrencesOfString:@"github://callName_?" withString:@""]];
            [[UIApplication sharedApplication] openURL:url];
        }])];
        [self presentViewController:alertController animated:YES completion:nil];
        decisionHandler(WKNavigationActionPolicyCancel);
    }else{
        decisionHandler(WKNavigationActionPolicyAllow);
    }
}

在已经收到response时决定是否允许或者取消页面的加载
① WKNavigationResponse描述了response的信息
② decisionHandler block在app已经决定允许或取消页面加载时调用
③ 这个block携带一个参数,是WKNavigationResponsePolicy枚举类型
④ WKNavigationResponsePolicy枚举类型包括WKNavigationResponsePolicyCancel取消和WKNavigationResponsePolicyAllow允许两个枚举值
⑤ 你可以立即调用该block或者保存block并在以后的时间异步调用它。
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
    NSString * urlStr = navigationResponse.response.URL.absoluteString;
    NSLog(@"当前跳转地址:%@",urlStr);
    //允许跳转
    decisionHandler(WKNavigationResponsePolicyAllow);
    //不允许跳转
    //decisionHandler(WKNavigationResponsePolicyCancel);
}

当WebView需要响应网页的登录请求时触发,在block中需要传入用户身份凭证
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
    //用户身份信息
    NSURLCredential * newCred = [[NSURLCredential alloc] initWithUser:@"user123" password:@"123" persistence:NSURLCredentialPersistenceNone];
    //为 challenge 的发送方提供 credential
    [challenge.sender useCredential:newCred forAuthenticationChallenge:challenge];
    completionHandler(NSURLSessionAuthChallengeUseCredential,newCred);
}

  • WKUIDelegate
    实现非常规界面或者警告框,确认框,输入框
    提供为网页展示native用户界面的方法。WebView用户界面通过实现这个协议来控制新窗口的打开,增强用户单击元素时显示的默认菜单项的表现,并执行其他用户界面相关的任务。这些方法可以通过处理JavaScript或其他插件内容来调用。默认每个WebView一个窗口,如果需要实现一个非常规用户界面,需要依靠WKUIDelegate来实现
创建一个新的WebView
① configuration是创建新WebView的配置
② navigationAction是触发本次调用的操作
③ windowFeatures是本次请求的窗口属性
④ 创建的新WebView一定要使用指定的configuration
⑤ WebKit将会在返回的WebView中加载请求
- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures;

显示一个JavScript 警告界面
① message是要显示的内容
② frame的JavaScript进程发起这次调用
③ 在警告界面被解除之后调用completionHandler来回调给页面
④ 为了安全,实现这个方法的时候需要注意到警告的内容是有一个特定的网站指定的,这里有一个简单的准则就是用frame.request.URL.host属性来标识这个警告。
⑤ 警告应该只有一个确定按钮
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
 UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"HTML的弹出框" message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:([UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler();
    }])];
    [self presentViewController:alertController animated:YES completion:nil];
}

显示一个JavScript 确认界面
① message是要显示的内容
② frame的JavaScript进程发起这次调用
③ 在确认界面被解除之后调用completionHandler来回调给页面,点击确定传YES,取消传NO
④ 为了安全,实现这个方法的时候需要注意到确认的内容是有一个特定的网站指定的,这里有一个简单的准则就是用frame.request.URL.host属性来标识这个确认界面
⑤ 确认界面应该只有两个按钮,典型的就是确认和取消
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler {
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:([UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(NO);
    }])];
    [alertController addAction:([UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(YES);
    }])];
    [self presentViewController:alertController animated:YES completion:nil];
 }

显示一个JavaScript 输入界面
① message是要显示的内容
② defaultText是在输入框中展示的默认文本
③ frame的JavaScript进程发起这次调用
④ 在输入界面被解除之后调用completionHandler来回调给页面,点击确定传输入框的文本,取消传nil
⑤ 为了安全,实现这个方法的时候需要注意到确认的内容是有一个特定的网站指定的,这里有一个简单的准则就是用frame.request.URL.host属性来标识这个确认界面
⑥ 确认界面应该只有两个按钮(典型的就是确认和取消)和一个输入框
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *result))completionHandler {
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:prompt message:@"" preferredStyle:UIAlertControllerStyleAlert];
    [alertController addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
        textField.text = defaultText;
    }];
    [alertController addAction:([UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(alertController.textFields[0].text?:@"");
    }])];
    [self presentViewController:alertController animated:YES completion:nil];
}

通知app网页的DOM窗口已经关闭
- (void)webViewDidClose:(WKWebView *)webView;
app应该从控件关系中移除这个WebView并重新安排界面的显示
例如关闭一个浏览器tab或者窗口

iOS 10.0+决定是否要预览指定的WKPreviewElementInfo
- (BOOL)webView:(WKWebView *)webView shouldPreviewElement:(WKPreviewElementInfo *)elementInfo;
返回NO会禁止本次预览
这个方法只会在元素有默认的预览时调用

iOS 10.0+当用户发出了预览操作(比如3D Touch按压)时调用
- (UIViewController *)webView:(WKWebView *)webView previewingViewControllerForElement:(WKPreviewElementInfo *)elementInfo defaultActions:(NSArray> *)previewActions;
elementInfo是相关的预览信息
previewActions是元素使用的默认的操作选项(预览时上拉可见),比如打开链接,添加到阅读列表,拷贝链接,共享。返回nil则使用默认的预览表现
如果返回一个视图控制器则预览会显示这个视图控制器,并且在用户触发pop操作(继续按压)时触发webView:commitPreviewingViewController: 的调用
如果要使用默认预览操作,应用程序必须在返回的视图控制器的previewActionItems实现中运行这些操作

iOS 10.0+预览时用户触发pop操作(继续按压)时调用
- (void)webView:(WKWebView *)webView commitPreviewingViewController:(UIViewController *)previewingViewController;
previewingViewController就是即将展示的预览页面
你需要将这个预览界面在界面中继续展示,以保持预览的连贯性
如果你在这里什么也不做,用户的pop操作将什么也不会改变

四.JS和原生的交互

  • JS调用原生

① 这个实现主要是依靠WKScriptMessageHandler协议类和WKUserContentController两个类
② WKUserContentController对象负责注册JS方法,设置处理接收JS方法的代理,代理遵守WKScriptMessageHandler,实现捕捉到JS消息的回调方法,详情可以看第一步中对这两个类的介绍。

 //这个类主要用来做JS和原生的交互管理
 WKUserContentController * wkUController = [[WKUserContentController alloc] init]; //注册一个name为jsToOcNoPrams的js方法,设置处理接收JS方法的代理
 [wkUController addScriptMessageHandler:self  name:@"jsToOcNoPrams"];
 [wkUController addScriptMessageHandler:self  name:@"jsToOcWithPrams"];
 config.userContentController = wkUController;

 注意:self遵守WKScriptMessageHandler协议,实现协议方法.代理是由WKUserContentControler设置
 //通过接收JS传出消息的name进行捕捉的回调方法 JS调OC
 - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    NSLog(@"name:%@\\\\n body:%@\\\\n frameInfo:%@\\\\n",message.name,message.body,message.frameInfo);
 }
  • 原生调用JS
  //原生调用JS  
  //changeColor()是JS方法名,completionHandler是异步回调block
  //改变HTML背景色 调用原生JS方法
  NSString *jsString = [NSString stringWithFormat:@"changeColor('%@')", @"Js参数"];
  [_webView evaluateJavaScript:jsString completionHandler:^(id _Nullable data, NSError * _Nullable error) {
  NSLog(@"改变HTML的背景色");
  }];
    
   //改变HTML字体大小 调用原生JS方法
   NSString *jsFont = [NSString stringWithFormat:@"document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust= '%d%%'", arc4random()%99 + 100];
  [_webView evaluateJavaScript:jsFont completionHandler:nil];

   //切换本地头像
  NSString * path =  [[NSBundle mainBundle] pathForResource:@"girl" ofType:@"png"];
   NSString *jsPicture = [NSString stringWithFormat:@"changePicture('%@','%@')", @"pictureId",path];
  [_webView evaluateJavaScript:jsPicture completionHandler:^(id _Nullable data, NSError * _Nullable error) {
   NSLog(@"切换本地头像");
  }];

五.监听加载进度条和title

  //添加监测网页加载进度的观察者
  [self.webView addObserver:self
                   forKeyPath:@"estimatedProgress"
                      options:0
                      context:nil];
  //添加监测网页标题title的观察者
  [self.webView addObserver:self
                   forKeyPath:@"title"
                      options:NSKeyValueObservingOptionNew
                      context:nil];

  //KVO 监听进度 必须实现此方法
  -(void)observeValueForKeyPath:(NSString *)keyPath
                     ofObject:(id)object
                       change:(NSDictionary *)change
                      context:(void *)context{
  if ([keyPath isEqualToString:NSStringFromSelector(@selector(estimatedProgress))]
        && object == _webView) {
       NSLog(@"网页加载进度 = %f",_webView.estimatedProgress);
        self.progressView.progress = _webView.estimatedProgress;
      if (_webView.estimatedProgress >= 1.0f) {
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                self.progressView.progress = 0;
            });
        } 
  }else if([keyPath isEqualToString:@"title"]
             && object == _webView){
        self.navigationItem.title = _webView.title;
  }else{
      [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
     }
  }

  //移除观察者
  [_webView removeObserver:self
                  forKeyPath:NSStringFromSelector(@selector(estimatedProgress))];
  [_webView removeObserver:self
                  forKeyPath:NSStringFromSelector(@selector(title))];

更多WKWebView使用还可以参考:
https://www.jianshu.com/p/833448c30d70
https://www.jianshu.com/p/4fa8c4eb1316
https://www.jianshu.com/p/91cfe58c032d

你可能感兴趣的:(WKWebView的使用)