再说WKWebView

2020年4月起,AppStore 将不再接受使用 UIWebView 的新应用程序
2020年12月起,AppStore不再接受使用 UIWebView 的应用程序更新。

初识 WKWebView
之前汇总过过关于 WKWebView 的相关内容,但再回首还是觉得有好多地方写得不清不楚,所以再细说一下。

细说之前解决两个问题
  1. 写一个实现 WKScripMessageHandler 的类,
    这个类的主要作用是解决 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;
}
//遵循WKScriptMessageHandler协议,必须实现如下方法,然后把方法向外传递
//通过接收JS传出消息的name进行捕捉的回调方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    
    if ([self.scriptDelegate respondsToSelector:@selector(userContentController:didReceiveScriptMessage:)]) {
        [self.scriptDelegate userContentController:userContentController didReceiveScriptMessage:message];
    }
}
@end
  1. COOKIE 问题的处理
//解决第一次进入的cookie丢失问题
- (NSString *)readCurrentCookieWithDomain:(NSString *)domainStr{
    
    NSHTTPCookieStorage * cookieJar = [NSHTTPCookieStorage sharedHTTPCookieStorage];
    NSMutableString * cookieString = [[NSMutableString alloc]init];
    for (NSHTTPCookie*cookie in [cookieJar cookies]) {
        [cookieString appendFormat:@"%@=%@;",cookie.name,cookie.value];
    }
    
    //删除最后一个“;”
    if ([cookieString hasSuffix:@";"]) {
        [cookieString deleteCharactersInRange:NSMakeRange(cookieString.length - 1, 1)];
    }
    
    return cookieString;
}
//解决 页面内跳转(a标签等)还是取不到cookie的问题
- (void)getCookie{
    //TODO: 3. cookie 是啥
    //取出cookie
    NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
    //js函数
    NSString *JSFuncString =
    @"function setCookie(name,value,expires)\
    {\
    var oDate=new Date();\
    oDate.setDate(oDate.getDate()+expires);\
    document.cookie=name+'='+value+';expires='+oDate+';path=/'\
    }\
    function getCookie(name)\
    {\
    var arr = document.cookie.match(new RegExp('(^| )'+name+'=([^;]*)(;|$)'));\
    if(arr != null) return unescape(arr[2]); return null;\
    }\
    function delCookie(name)\
    {\
    var exp = new Date();\
    exp.setTime(exp.getTime() - 1);\
    var cval=getCookie(name);\
    if(cval!=null) document.cookie= name + '='+cval+';expires='+exp.toGMTString();\
    }";
    
    //拼凑js字符串
    NSMutableString *JSCookieString = JSFuncString.mutableCopy;
    for (NSHTTPCookie *cookie in cookieStorage.cookies) {
        NSString *excuteJSString = [NSString stringWithFormat:@"setCookie('%@', '%@', 1);", cookie.name, cookie.value];
        [JSCookieString appendString:excuteJSString];
    }
    //执行js
    [_webView evaluateJavaScript:JSCookieString completionHandler:nil];
    
}

懒加载
- (WKWebView*)webView {
    
    if (!_webView) {
        
        // ============A 部分============
        //先实例化配置类
        WKWebViewConfiguration * config = [[WKWebViewConfiguration alloc]init];
        
        // 创建设置对象
        WKPreferences *preference = [[WKPreferences alloc]init];
        //最小字体大小 当将javaScriptEnabled属性设置为NO时,可以看到明显的效果
        preference.minimumFontSize = 0;
        //设置是否支持javaScript 默认是支持的
        preference.javaScriptEnabled = YES;
        // 在iOS上默认为NO,表示是否允许不经过用户交互由javaScript自动打开窗口
        preference.javaScriptCanOpenWindowsAutomatically = YES;
        config.preferences = preference;
        
        
        // 是使用h5的视频播放器在线播放, 还是使用原生播放器全屏播放
        config.allowsInlineMediaPlayback = NO;
        //设置视频是否需要用户手动播放  设置为NO则会允许自动播放
        config.mediaTypesRequiringUserActionForPlayback = YES;
        //设置是否允许画中画技术 在特定设备上有效
        config.allowsPictureInPictureMediaPlayback = YES;
        //设置请求的User-Agent信息中应用程序名称 iOS9后可用
        config.applicationNameForUserAgent = @"ChinaDailyForiPad";
        

        //这个类主要用来做native与JavaScript的交互管理
        WKUserContentController * wkUController =[[WKUserContentController alloc]init];
        config.userContentController = wkUController;
        
        //JS 对 iOS端发消息通过
        //window.webkit.messageHandlers..postMessage()
        //JS 对 安卓发消息通过
        //(window.android.方法名();
        
        //当然首先需要对name方法名进行注册
        //注册一个name为jsToOcNoPrams的js方法
        [wkUController addScriptMessageHandler:self
                                          name:@"jsToOcNoPrams"];
        [wkUController addScriptMessageHandler:self
                                          name:@"jsToOcWithPrams"];
        
        
        //上面的代码设代理为self,也可以,但可能会有内存不释放的问题
        //为了解决内存不释放的问题,有两种解决方法
        //1. 自定义的WKScriptMessageHandler
        /*
        WeakWebViewScriptMessageDelegate *weakScriptMessageDelegate = [[WeakWebViewScriptMessageDelegate alloc] initWithDelegate:self];
        [wkUController addScriptMessageHandler:weakScriptMessageDelegate
                                          name:@"jsToOcNoPrams"];
        [wkUController addScriptMessageHandler:weakScriptMessageDelegate
                                          name:@"jsToOcWithPrams"]
         */
        
         //2. 在 ViewWillAppear和 WillDisappear中成对出现添加和删除
        /*
         - (void)viewWillAppear:(BOOL)animated {
         [super viewWillAppear:animated];
         [self.webview.configuration.userContentController addScriptMessageHandler:self name:@"YourFuncName"];
         }
         - (void)viewWillDisappear:(BOOL)animated {
         [super viewWillDisappear:animated];
         [self.webview.configuration.userContentController removeScriptMessageHandlerForName:@"YourFuncName"];
         }
         */
        
        //以下代码适配文本大小
        NSString *jSString = @"var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);";
        //用于进行JavaScript注入代码,位置,所有框架还是主框架
        WKUserScript *wkUScript
        = [[WKUserScript alloc] initWithSource:jSString
                                 injectionTime:WKUserScriptInjectionTimeAtDocumentEnd
                              forMainFrameOnly:YES];
        [config.userContentController addUserScript:wkUScript];
        
        //用于进行JavaScript注入代码,位置,所有框架还是主框架
        NSString *jSString2 = @"alert(\"WKUserScript 注入 js \")";
        WKUserScript *wkUScript2
        = [[WKUserScript alloc] initWithSource:jSString2
                                 injectionTime:WKUserScriptInjectionTimeAtDocumentEnd
                              forMainFrameOnly:YES];
        [config.userContentController addUserScript:wkUScript];
        [config.userContentController addUserScript:wkUScript2];

        
        
        // ============B 部分============
        //创建webView
        _webView
        = [[WKWebView alloc]initWithFrame:CGRectMake(0, 0, kWidth, kHeight)
                            configuration:config];
        //UI代理
        _webView.UIDelegate = self;
        //导航代理
        _webView.navigationDelegate = self;
        
        _webView.allowsLinkPreview = YES;//允许预览链接
        // 是否允许手势左滑返回上一级, 类似导航控制的左滑返回
        _webView.allowsBackForwardNavigationGestures = YES;
        //可返回的页面列表, 存储已打开过的网页
        //WKBackForwardList * backForwardList = [_webView backForwardList];
        
        
        
        //1.加载本地页面
        NSString* htmlPath = [[NSBundle mainBundle] pathForResource:@"JStoOC"
                                                             ofType:@"html"];
        NSString* appHtml = [NSString stringWithContentsOfFile:htmlPath
                                                      encoding:NSUTF8StringEncoding
                                                         error:nil];
        NSURL *baseURL = [NSURL fileURLWithPath:htmlPath];
        [_webView loadHTMLString:appHtml baseURL:baseURL];
        
        //2.加载远程页面
        //NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
        //[request addValue:[self readCurrentCookieWithDomain:@"http://www.chinadaily.com.cn"] forHTTPHeaderField:@"Cookie"];
        //[_webView loadRequest:request];
        
    }
    return _webView;
}
- (UIProgressView *)progressView {
    if (!_progressView){
        _progressView = [[UIProgressView alloc] initWithFrame:CGRectMake(0, 88 + 2, self.view.frame.size.width, 2)];
        _progressView.tintColor = [UIColor blueColor];
        _progressView.trackTintColor = [UIColor clearColor];
    }
    return _progressView;
}
添加观察者
   //添加监测网页加载进度的观察者
    [_webView addObserver:self
               forKeyPath:@"estimatedProgress"
                  options:NSKeyValueObservingOptionNew
                  context:nil];
    //添加监测网页标题title的观察者
    [_webView addObserver:self
               forKeyPath:@"title"
                  options:NSKeyValueObservingOptionNew
                  context:nil];
观察者回调方法

-(void)observeValueForKeyPath:(NSString *)keyPath
                     ofObject:(id)object
                       change:(NSDictionary *)change
                      context:(void *)context{
    
    if ([keyPath isEqualToString:@"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];
    }
}
移除
- (void)dealloc{
    
    //移除注册的js方法
    [[_webView configuration].userContentController removeScriptMessageHandlerForName:@"jsToOcNoPrams"];
    [[_webView configuration].userContentController removeScriptMessageHandlerForName:@"jsToOcWithPrams"];
    //移除观察者
    [_webView removeObserver:self
                  forKeyPath:NSStringFromSelector(@selector(estimatedProgress))];
    [_webView removeObserver:self
                  forKeyPath:NSStringFromSelector(@selector(title))];
}

WKUIDelegate 主要处理JS脚本,确认框,警告框等

//通过这种弹窗拦截可以实现 JS 对 OC 的调用
JS调用OC方法1

//弹出警告框
// message           警告框中的内容
// completionHandler 警告框消失调用
- (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:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler();
    }])];
    [self presentViewController:alertController animated:YES completion:nil];
    
}
//确认框
//JavaScript调用confirm方法后回调的方法 confirm是js中的确定框,
//需要在block中把用户选择的情况传递进去
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message
initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler{
    
    UIAlertController *alertController
    = [UIAlertController alertControllerWithTitle:@"HTML的确认弹窗"
                                          message:message?:@""
                                   preferredStyle:UIAlertControllerStyleAlert];
    
    [alertController addAction:([UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(NO);
    }])];
    [alertController addAction:([UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(YES);
    }])];
    [self presentViewController:alertController animated:YES completion:nil];
}

// 输入框
//JavaScript调用prompt方法后回调的方法 prompt是js中的输入框
//需要在block中把用户输入的信息传入
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable))completionHandler{
    
    UIAlertController *alertController
    = [UIAlertController alertControllerWithTitle:prompt
                                          message:@"HTML的输入窗"
                                   preferredStyle:UIAlertControllerStyleAlert];
    [alertController addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
        textField.text = defaultText;
    }];
    [alertController addAction:([UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(alertController.textFields[0].text?:@"");
    }])];
    [self presentViewController:alertController animated:YES completion:nil];
}
// 页面是弹出窗口 _blank 处理
- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {
    
    if (!navigationAction.targetFrame.isMainFrame) {
        [webView loadRequest:navigationAction.request];
    }
    return nil;
}
WKNavigationDelegate 主要处理一些跳转、加载处理操作
//页面开始加载时调用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
    NSLog(@"页面开始加载时调用");
}
// 当内容开始返回时调用
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
    NSLog(@"当内容开始返回时调用");
}
// 页面加载失败时调用
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error{
    NSLog(@"页面加载失败时调用");
    [self.progressView setProgress:0.0f animated:NO];
}
//页面加载完成之后调用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation{
    NSLog(@"页面加载完成之后调用");
    [self getCookie];

}
//提交发生错误时调用
- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error{
    
    NSLog(@"提交发生错误时调用");
    [self.progressView setProgress:0.0f animated:NO];
    
}
// 接收到服务器跳转请求即服务重定向时之后调用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation{
    NSLog(@"接收到服务器跳转请求即服务重定向时之后调用");
}

#pragma mark  JS调用OC方法2
//通过这种办法也可以实现 JS 对 OC 的调用
//根据WebView对于即将跳转的HTTP请求头信息和相关信息来决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
    
    NSLog(@"根据请求头信息和相关信息来决定是否跳转");
    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相关信息来决定是否可以跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(nonnull WKNavigationResponse *)navigationResponse decisionHandler:(nonnull void (^)(WKNavigationResponsePolicy))decisionHandler{
    
    NSLog(@"收到响应后决定是否跳转");
    NSString * urlStr = navigationResponse.response.URL.absoluteString;
    NSLog(@"当前跳转地址:%@",urlStr);
    //允许跳转
    decisionHandler(WKNavigationResponsePolicyAllow);
    //不允许跳转
    //decisionHandler(WKNavigationResponsePolicyCancel);
  
}
//需要响应身份验证时调用 同样在block中需要传入用户身份凭证
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler{
    
    NSLog(@"需要响应身份验证时调用");
    //用户身份信息
    NSURLCredential * newCred
    = [[NSURLCredential alloc] initWithUser:@"user123"
                                   password:@"123"
                                persistence:NSURLCredentialPersistenceNone];
    //为 challenge 的发送方提供 credential
    [challenge.sender useCredential:newCred forAuthenticationChallenge:challenge];
    completionHandler(NSURLSessionAuthChallengeUseCredential,newCred);
}
//进程被终止时调用
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView{
    NSLog(@"进程被终止时调用");
}
WKScriptMessageHandler

JS调用OC方法3

//遵守WKScriptMessageHandler协议,代理是由WKUserContentControl设置
//这个协议类专门用来处理监听JavaScript方法从而调用原生OC方法,
//被自定义的WKScriptMessageHandler在回调方法里通过代理回调回来,绕了一圈就是为了解决内存不释放的问题
- (void)userContentController:(WKUserContentController *)userContentController
      didReceiveScriptMessage:(WKScriptMessage *)message{
    
    //通过接收JS传出消息的name进行捕捉的回调方法
     NSLog(@"name:%@\\\\n body:%@\\\\n frameInfo:%@\\\\n",
           message.name,
           message.body,
           message.frameInfo);
    NSDictionary * parameter = message.body;
    //JS调用OC
    if([message.name isEqualToString:@"jsToOcNoPrams"]){
        UIAlertController *alertController
        = [UIAlertController alertControllerWithTitle:@"js调用到了oc"
                                              message:@"不带参数"
                                       preferredStyle:UIAlertControllerStyleAlert];
        [alertController addAction:([UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        }])];
        [self presentViewController:alertController animated:YES completion:nil];
        
    }else if([message.name isEqualToString:@"jsToOcWithPrams"]){
        UIAlertController *alertController
        = [UIAlertController alertControllerWithTitle:@"js调用到了oc"
                                              message:parameter[@"params"]
                                       preferredStyle:UIAlertControllerStyleAlert];
        [alertController addAction:([UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        }])];
        [self presentViewController:alertController animated:YES completion:nil];
    }

}
- (void)showLoginView{
    
    NSLog(@"弹出登陆页面");
}

-(void)gotodetial:(NSString *)url {
    
    NSLog(@"跳转到详情页");
}
OC 调用 JS 方法

OC调用 JS 方法比较简单直接,可以直接调用,
也可以通过 JS 注入的方法

- (void)ocToJs{
    
    //NOTE: 方法不会阻塞线程,而且它的回调代码块总是在主线程中运行
    //changeColor()是JS方法名,completionHandler是异步回调block
    NSString *jsString = [NSString stringWithFormat:@"changeColor('%@')", @"Js颜色参数"];
    [_webView evaluateJavaScript:jsString
               completionHandler:^(id _Nullable data, NSError * _Nullable error) {
                   
        NSLog(@"改变HTML的背景色");
    }];
    
    //改变字体大小 调用原生JS方法
    //getElementsByTagName 带有指定标签名的对象的集合
    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(@"切换本地头像");
    }];
    
    
    
    //Tips: 参数传弟异常
    NSString *jsMothod1
    =[NSString stringWithFormat:@"build_visitor_group(%@)",@"15066676292965107"];
    [_webView evaluateJavaScript:jsMothod1
               completionHandler:nil];
    //可能系统首先将字符串格式的数字转化成了基础数据类型,
    //再将基础数据类型的数字传到前端转化成字符串,从而导致了数字精度缺失发生变化15066676292965108
    //可以在数字两侧加上双引号能够防止数字自动转换成基础数据类型,强制将其声明为字符串类型
    NSString *jsMothod2
    =[NSString stringWithFormat:@"build_visitor_group(\"%@\")",@"15066676292965107"];
    [_webView evaluateJavaScript:jsMothod2
               completionHandler:nil];
    
}

你可能感兴趣的:(再说WKWebView)