WKWebView从入门到放弃 (一 入门)
$[timeformat('2021-10-08T10:51:39+08:00')]

WKWebView 初始化

第一步:导入头文件 声明webview属性

// 导入头文件
#import <WebKit/WebKit.h>
// 声明webView属性
@property (nonatomic, strong) WKWebView *webView;

第二步 : 初始化webView

webview初始化位置**【尽量提前】**,如果过项目需要,甚至可以在APP启动后的某个特殊时机,预初始化一个全局webView。这样能够提前让项目初始化web内核,以减少初次进入webView的等待时间。 牺牲一定的性能,换取web启动时间,根据需求决定,我这里是放到了ViewController的ViewDidload方法中了。

// 使用webconfig方式初始化webView
_webView = [[WKWebView alloc] initWithFrame:frame configuration:self.webConfig];
 // UI代理 主要是 alert 等弹窗的代理;
 // 这里你可以设置一个专门的NSObject类遵守WKUIDelegate协议来处理回调方法 设置: _webView.UIDelegate = self.uiDelegater;
 _webView.UIDelegate = self;
 // 导航代理 同理 可设置一个代理类处理 导航跳转问题 继承 WKNavigationDelegate 协议 
 _webView.navigationDelegate = self;
 // 是否允许手势左滑返回上一级, 类似导航控制的左滑返回
 _webView.allowsBackForwardNavigationGestures = YES;
_webView.backgroundColor = [UIColor clearColor];
// scrollView 滚动速度 UIScrollViewDecelerationRateFast:快速 UIScrollViewDecelerationRateNormal:正常速度
_webView.scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;
//_webView.customUserAgent = @"userAgent";// 自定义请求 userAgent
//_webView.allowsLinkPreview = YES;// 默认 不支持按压预览
    
//添加监测网页加载进度的观察者
 [_webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:@"LXWKWebViewController"];
//添加监测网页标题title的观察者
 [_webView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:nil];

另外还有初始化方法:

// 通过制定URL初始化webView 太过固定
 - (instancetype)initWithURL:(NSURL *)URL;
 
 // 可定制webView的 网络策略 和 超时时间 缺点同上
 - (instancetype)initWithURL:(NSURL *)URL cachePolicy:(NSURLRequestCachePolicy)cachePolicy timeoutInterval:(NSTimeInterval)timeoutInterval NS_DESIGNATED_INITIALIZER;

第三步:加载网络请求

加载网页地址,如果需要记得在info.plist文件中提前开启ATS, NSAllowsArbitraryLoads --> YES,允许http的网页请求。

webRequest请求时机

某些机型中 loadRequest 方法 放在viewDidload方法中去执行,会出现webView白屏现象,所以我这边放到了viewwillappear方法中。这里要记得判断是否是初次viewwillappear,避免网页频繁刷新。

if (self.url && !_request){
    //忽略本地缓存
    _request = [NSURLRequest requestWithURL:self.url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:60];
    [self.webView loadRequest:_request];
}

webRequest请求参数

  • 需要注意的属性时 NSURLRequestCachePolicy
typedef NS_ENUM(NSUInteger, NSURLRequestCachePolicy)
{
    NSURLRequestUseProtocolCachePolicy = 0,// 默认 启用网页缓存,APP重启后会清空缓存
    NSURLRequestReloadIgnoringLocalCacheData = 1,// 忽略本地缓存,每次都用最新的网络数据
    NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4,// 不仅忽略本地缓存,同时也忽略代理服务器或其他中间介质目前已有的、协议允许的缓存
    NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,// 同上
    NSURLRequestReturnCacheDataElseLoad = 2,// 无论缓存是否过期,先使用本地缓存数据。如果缓存中没有请求所对应的数据,那么从原始地址加载数据
    NSURLRequestReturnCacheDataDontLoad = 3,// 无论缓存是否过期,先使用本地缓存数据。如果缓存中没有请求所对应的数据,那么放弃从原始地址加载数据,请求视为失败(即:“离线”模式)。
    NSURLRequestReloadRevalidatingCacheData = 5,// 从原始地址确认缓存数据的合法性后,缓存数据就可以使用,否则从原始地址加载。
};
  • 超时时间不需要多说 一般最多 15-30s为宜

网页偏好设置:WKWebViewConfiguration

上代码 Getter Function

- (WKWebViewConfiguration *)webConfig{
    if (!_webConfig) {
        _webConfig = [[WKWebViewConfiguration alloc]init];
        _webConfig.preferences.javaScriptEnabled = YES;//打开js交互
        
        // 是使用h5的视频播放器在线播放, 还是使用原生播放器全屏播放
        _webConfig.allowsInlineMediaPlayback = YES;
        //设置视频是否需要用户手动播放  设置为NO则会允许自动播放
        _webConfig.requiresUserActionForMediaPlayback = YES;
        //设置是否允许画中画技术 在特定设备上有效
        _webConfig.allowsPictureInPictureMediaPlayback = YES;
        //设置请求的User-Agent信息中应用程序名称 iOS9后可用
        _webConfig.applicationNameForUserAgent = @"ChinaDailyForiPad";
        // 设置 preferences 主要是 网页浏览属性 支持js 交互什么的
        _webConfig.preferences = self.webPreference;
        // OC JS交互的方法
        _webConfig.userContentController = self.webUserContentController;
        // 识别h5中的邮箱电话识别
        if (@available(iOS 10.0, *)) {
            _webConfig.dataDetectorTypes = UIDataDetectorTypeAll;
        } else {
            // Fallback on earlier versions
        }
    }
    return _webConfig;
}

- (WKWebViewConfiguration *)webConfig{
    if (!_webConfig) {
        _webConfig = [[WKWebViewConfiguration alloc]init];        
        // 是使用h5的视频播放器在线播放, 还是使用原生播放器全屏播放
        _webConfig.allowsInlineMediaPlayback = YES;
        //设置视频是否需要用户手动播放  设置为NO则会允许自动播放
        _webConfig.requiresUserActionForMediaPlayback = YES;
        //设置是否允许画中画技术 在特定设备上有效
        _webConfig.allowsPictureInPictureMediaPlayback = YES;
        //设置请求的User-Agent信息中应用程序名称 iOS9后可用
        _webConfig.applicationNameForUserAgent = @"ChinaDailyForiPad";
        // 设置 preferences 主要是 网页浏览属性 支持js 交互什么的
        _webConfig.preferences = self.webPreference;
        // OC JS交互的方法
        _webConfig.userContentController = self.webUserContentController;
        // 识别h5中的邮箱电话识别
        if (@available(iOS 10.0, *)) {
            _webConfig.dataDetectorTypes = UIDataDetectorTypeAll;
        } else {
            // Fallback on earlier versions
        }
    }
    return _webConfig;
}
- (WKPreferences *)webPreference{
    if (!_webPreference) {
        // 创建设置对象
        _webPreference = [[WKPreferences alloc]init];
        //最小字体大小 当将javaScriptEnabled属性设置为NO时,可以看到明显的效果
        _webPreference.minimumFontSize = 10;
        //设置是否支持javaScript 默认是支持的
        _webPreference.javaScriptEnabled = YES;
        // 在iOS上默认为NO,表示是否允许不经过用户交互由javaScript自动打开窗口
        _webPreference.javaScriptCanOpenWindowsAutomatically = YES;
        if (@available(iOS 13.0, *)) {// 恶意软件警告
            _webPreference.fraudulentWebsiteWarningEnabled = YES;
        }
    }
    return _webPreference;
}
- (WKUserContentController *)webUserContentController{
    if (!_webUserContentController) {
        _webUserContentController = [[WKUserContentController alloc] init];
    }
    return _webUserContentController;
}

WKWebViewConfiguration 结构

首先看一下这个类的结构

  • WKWebViewConfiguration:这个类专门用来配置WKWebView
    • WKPreference:这个类用来进行相关webView【偏好设置】
    • WKProcessPool:用来配置进程池,与网页视图的资源共享有关
    • WKUserContentController:主要用来做native与JavaScript的交互管理
      • WKUserScript:用于进行JavaScript注入
      • WKScriptMessageHandler:用来处理JavaScript回调native的方法

WKNavigationDelegate

网页状态回调

#pragma mark - **************** WKNavigationDelegate
// 页面开始加载时调用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {}
// 页面加载失败时调用
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error {}
// 当内容开始返回时调用
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {}
// 页面加载完成之后调用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {}
//提交发生错误时调用
- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error {}
// 接收到服务器跳转请求即服务重定向时之后调用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation {}

WKNavigationAction 网页活动回调

// 根据WebView对于即将跳转的HTTP请求头信息和相关信息来决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    NSString *urlStr = navigationAction.request.URL.absoluteString;
    urlStr = [urlStr stringByRemovingPercentEncoding];
    // 处理 H5 重定向问题
    NSString *supportDomain = @"www.***.com";

    WKFrameInfo *frameInfo = navigationAction.targetFrame;
    if (![frameInfo isMainFrame] && [urlStr containsString:supportDomain]) {
        [webView loadRequest:navigationAction.request];
        self.url = navigationAction.request.URL;
    }
    // 自定义规则 拦截内部跳转
    if ([urlStr hasPrefix:schemeHeader]) {
        [ControllerUtils showViewWithURL:urlStr];
        
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    // 微信跳转
    if ([urlStr hasPrefix:@"weixin://"]) {
        if ([[UIApplication sharedApplication] canOpenURL:navigationAction.request.URL]) {
            [[UIApplication sharedApplication] openURL:navigationAction.request.URL];
        }
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    NSLog(@"发送跳转请求:%@",urlStr);
    // 允许跳转
    decisionHandler(WKNavigationActionPolicyAllow);
}
// 根据客户端受到的服务器响应头以及response相关信息来决定是否可以跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{
    NSString * urlStr = navigationResponse.response.URL.absoluteString;
    urlStr=[urlStr stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];//个人情况,url里面会加入中文
    NSLog(@"当前跳转地址:%@",urlStr);
    //允许跳转
    decisionHandler(WKNavigationResponsePolicyAllow);
    //不允许跳转
    //decisionHandler(WKNavigationResponsePolicyCancel);
}

/// 需要响应身份验证时调用 同样在block中需要传入用户身份凭证  ios 10.3 下 崩溃
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler{
    // 判断服务器采用的验证方法
    if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust) {
         // 如果没有错误的情况下 创建一个凭证,并使用证书
        if (challenge.previousFailureCount == 0) {
            NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
        }else {
        // 验证失败,取消本次验证
            completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
        }
    }else {
        completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);

    }
}

//进程被终止时调用
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView{
//    [webView reload];
}

WKUIDelegate UI代理

#pragma mark - **************** WKUIDelegate
/**
 *  web界面中有弹出警告框时调用
 *
 *  @param webView           实现该代理的webview
 *  @param message           警告框中的内容
 *  @param completionHandler 警告框消失调用
 */
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {

    // 为避免 scheme 跳转界面后 alert弹出弹窗 崩溃问题,做一个兼容(VC A,模态跳转到B后,不能再由A模态跳转B了)
    if (![[self appKeyWindowTopVC] isKindOfClass:[self.vcDelegate class]]) {
        completionHandler();
        return;
    }
    
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:([UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler();
    }])];
    [self.vcDelegate presentViewController:alertController animated:YES completion:nil];

}
// ------------- 确认框
//JavaScript调用confirm方法后回调的方法 confirm是js中的确定框,需要在block中把用户选择的情况传递进去
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{
    // 同上
    if (![[self appKeyWindowTopVC] isKindOfClass:[self.vcDelegate class]]) {
        completionHandler(NO);
        return;
    }
    
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" 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.vcDelegate 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{
    // 同上
    if (![[self appKeyWindowTopVC] isKindOfClass:[self.vcDelegate class]]) {
        completionHandler(@"");
        return;
    }
    
    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.vcDelegate 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;
}

#pragma mark - **************** tool
- (UIViewController *)appKeyWindowTopVC{
    UITabBarController *rootVC = (UITabBarController*)[(AppDelegate *)[UIApplication sharedApplication].delegate window].rootViewController;
    UIViewController *presentedVC = rootVC;
    while (presentedVC.presentedViewController) {
        presentedVC = presentedVC.presentedViewController;
    }
    return presentedVC;
}

到这里,WKWebView基本上就可以使用了。