WKWebView在工作中的使用广泛,所以想总结一下方便日后使用,直接上代码
.h文件
#import <WebKit/WebKit.h>
#import "CMWeakScriptMessageDelegate.h"
typedef void(^JYWebProgressEventBlock)(NSInteger tag);
@interface CMBaseWebViewController : UIViewController<WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler>
/** 网页 */
@property (nonatomic, strong) WKWebView *webView;
/** 进度条 */
@property (strong, nonatomic) UIProgressView *progressView;
/** 返回 */
@property(nonatomic,assign)BOOL canGoBack;
/** 前进 */
@property(nonatomic,assign)BOOL canGoForward;
@property(nonatomic,copy) JYWebProgressEventBlock blockProgress;
@property (nonatomic, strong)NSDictionary *baseUADic;
-(NSString*)DataTOjsonString:(id)object;
/**清缓存*/
- (void)cleanWebViewCache;
.m文件
#import "CMBaseWebViewController.h"
@interface CMBaseWebViewController ()
@end
@implementation CMBaseWebViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创建webView
[self setUpWebView];
// 创建进度条
[self setUpProgressView];
}
/**
* 布局
*/
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
}
-(NSString*)DataTOjsonString:(id)object
{
NSString *jsonString = nil;
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:object
options:NSJSONWritingPrettyPrinted // Pass 0 if you don't care about the readability of the generated string
error:&error];
if (! jsonData) {
NSLog(@"Got an error: %@", error);
} else {
jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}
return jsonString;
}
/**
* 创建WebView
*/
- (void)setUpWebView
{
//进行配置控制器
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
//实例化对象
configuration.userContentController = [[WKUserContentController alloc] init];
//重要:调用JS方法
[configuration.userContentController addScriptMessageHandler:(id<WKScriptMessageHandler>)[[CMWeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"jsCallbackMethod"];
[configuration.userContentController addScriptMessageHandler:(id<WKScriptMessageHandler>)[[CMWeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"ios_pop"];
[configuration.userContentController addScriptMessageHandler:(id<WKScriptMessageHandler>)[[CMWeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"updatePageTitle"];
[configuration.userContentController addScriptMessageHandler:(id<WKScriptMessageHandler>)[[CMWeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"jsCallbackMethodResult"];
// 创建webView
self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];
self.webView.UIDelegate = self;
self.webView.navigationDelegate = self;
self.webView.frame = self.view.bounds;
// NSDictionary *dic = @{
// @"appType":AppType,
// @"appNo":[JYKeyChainManage readValueWithIdentifier:KEY_IN_KEYCHAIN_UUID],
// @"appVersion":AppVersion,
// @"token":[JYUser sharedInstance].token
// };
//
//
// self.webView.customUserAgent = [self DataTOjsonString:dic];
[self.view addSubview:self.webView];
// KVO
[self.webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil];
[self.webView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:nil];
}
/**
* 创建进度条
*/
- (void)setUpProgressView
{
self.progressView = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleBar];
self.progressView.frame = CGRectMake(0, 0, kDeviceWidth, 0);
self.progressView.progressTintColor = HRGB(@"43C01C");
[self.view addSubview:self.progressView];
}
/**
* KVO
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"estimatedProgress"]) {
if (object == _webView) {
[self.progressView setAlpha:1.0f];
[self.progressView setProgress:self.webView.estimatedProgress animated:YES];
if(self.webView.estimatedProgress >= 1.0f) {
[UIView animateWithDuration:0.3 delay:0.3 options:UIViewAnimationOptionCurveEaseIn animations:^{
[self.progressView setAlpha:0.0f];
} completion:^(BOOL finished) {
[self.progressView setProgress:0.0f animated:NO];
}];
}
if (self.blockProgress) {
self.blockProgress(0);
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
} else if ([keyPath isEqualToString:@"title"]) {
if (object == self.webView) {
if (self.webView.title.length) {
self.navigationItem.title = self.webView.title;
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
//重要:由子类实现具体内容
}
#pragma mark - WKNavigationDelegate
// 对于HTTPS的都会触发此代理,如果不要求验证,传默认就行
// 如果需要证书验证,与使用AFN进行HTTPS证书验证是一样的
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *__nullable credential))completionHandler
{
completionHandler(NSURLSessionAuthChallengeUseCredential, nil);
}
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
self.canGoBack = webView.canGoBack;
self.canGoForward = webView.canGoForward;
}
- (void)popVC
{
if ((self.canGoBack == NO && self.canGoForward == NO) || (self.canGoBack == NO && self.canGoForward == YES)) {
[self.navigationController popViewControllerAnimated:YES];
return;
}
if (self.canGoBack == YES && self.canGoForward == NO) {
[self.webView goBack];
}
}
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation
{
[CMDefaultInfoViewTool dismissFromView:self.webView];
}
// 全局:处理拨打电话以及Url跳转等等
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
NSURL *URL = navigationAction.request.URL;
NSString *scheme = [URL scheme];
if ([scheme isEqualToString:@"tel"]) {
NSString *resourceSpecifier = [URL resourceSpecifier];
NSString *callPhone = [NSString stringWithFormat:@"telprompt://%@", resourceSpecifier];
/// 防止iOS 10及其之后,拨打电话系统弹出框延迟出现
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:callPhone]];
});
}
decisionHandler(WKNavigationActionPolicyAllow);
}
- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error
{
[CMDefaultInfoViewTool showLoadFailureView:self.webView];
}
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation
{
}
#pragma mark - WKUIDelegate
//- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
//{
// UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"测试" message:message preferredStyle:UIAlertControllerStyleAlert];
// [alert addAction:[UIAlertAction actionWithTitle:@"关闭" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
// completionHandler();
// }]];
//
// [self presentViewController:alert animated:YES completion:nil];
//}
/**
* 控制器销毁
*/
- (void)dealloc
{
JYLog(@"%@ ----- dealloc", [self class])
[self.progressView setAlpha:0.0f];
[self.progressView removeFromSuperview];
[self.webView removeObserver:self forKeyPath:@"estimatedProgress"];
[self.webView removeObserver:self forKeyPath:@"title"];
}
/**
* 清除缓存
*/
- (void)cleanWebViewCache
{
if ([[UIDevice currentDevice] systemVersion].floatValue >= 9.0) {
NSSet *websiteDataTypes = [WKWebsiteDataStore allWebsiteDataTypes];
NSDate *dateFrom = [NSDate dateWithTimeIntervalSince1970:0];
[[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes modifiedSince:dateFrom completionHandler:^{
}];
} else {
NSString *libraryPath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) firstObject];
NSString *cookiesFolderPath = [libraryPath stringByAppendingString:@"/Cookies"];
NSError *errors;
[[NSFileManager defaultManager] removeItemAtPath:cookiesFolderPath error:&errors];
}
}
@end
工作中遇到的问题:
- 1.接第三方sdk的时候,webView显示的title有问题,导致体验不好,比如:抓取到的title显示为: “xxx数据爬虫系统”这个时候为了体验更好我们会采用KVO来判断当前抓取到的title是什么并且做相对应的控制
- (void)viewDidLoad {
//监听title,因为会先显示title:“XXX数据爬虫系统”,所以要把此title隐藏
[self.webView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:NULL];
}
对应的实现方法:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"title"]){
if (object == self.webView) {
if (![self.webView.title isEqualToString:@"XXX数据爬虫系统"]) {
self.title = self.webView.title;
}
}else{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
}
当然我们还要移除KVO:
-(void)dealloc {
[self.webView removeObserver:self forKeyPath:@"title"];
}
潜在的问题有可能出现在dealloc中对KVO的注销上。当对同一个keypath进行两次removeObserver时会导致程序crash,这种情况常常出现在父类有一个KVO,父类在dealloc中remove了一次,子类又remove了一次的情况下。不过我们可以通过代码中context字段来标识出到底KVO是superClass注册的,还是self注册的。我们可以分别在父类以及本类中定义各自的context字符串,比如在本类中定义context为@"SonContext";然后在dealloc中remove observer时指定移除的自身添加的observer。这样iOS就能知道移除的是自己的KVO,而不是父类中的KVO,避免二次remove造成crash。
- 2.当我们接入第三方SDK时其中有个问题就是在本app内打开淘宝应用,但是iOS加载出来的页面无法显示调到淘宝按钮,android却可以显示,我们通过重写UserAgent解决这个问题。
self.webView.customUserAgent = [NSString stringWithFormat:@"%@%@",@"Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1/",[JYCommonObj convertToJsonData:dicM]];
- 3.还有个问题就是我们要判断本机是否已经安装了淘宝app,同样的问题android可以直接判断,但是iOS却无法直接判断我们要在代理方法里截取相关URL来做本地判断
-(void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
//'stringByReplacingPercentEscapesUsingEncoding:' is deprecated: first deprecated in iOS 9.0 - Use -stringByRemovingPercentEncoding instead, which always uses the recommended UTF-8 encoding.
//NSString *urlStr = [navigationAction.request.URL.absoluteString stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSString *urlStr = [navigationAction.request.URL.absoluteString stringByRemovingPercentEncoding];
NSLog(@"decidePolicyForNavigationAction, navigationAction.request.URL = %@", urlStr);
if ([urlStr containsString:@"taobao://"]) {
// 1.获取应用程序App-B的Page1页面的URL
NSURL *appBUrl = [NSURL URLWithString:[NSString stringWithFormat:@"%@",urlStr]];
// 2.判断手机中是否安装了对应程序
if ([[UIApplication sharedApplication] canOpenURL:appBUrl]) {
// 3. 打开应用程序App-B的Page1页面
[[UIApplication sharedApplication] openURL:appBUrl];
} else {
NSLog(@"没有安装");
}
}
decisionHandler(WKNavigationActionPolicyAllow);
}
- 4.配合h5测试弹框显示某些信息,不写此方法就算h5写了弹框iOS客户端也无法显示
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"测试" message:message preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"关闭" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
completionHandler();
}]];
[self presentViewController:alert animated:YES completion:nil];
}
- 5.与后台交互的回调方法: 具体交互规则要与h5定义好。
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
[super userContentController:userContentController didReceiveScriptMessage:message];
WEAKSELF
NSLog(@"%@",message.body);
NSString *str = (NSString*)message.body;
NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary * dic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
if ([dic[@"type"] isEqualToString:@"gxb_success"]) {
NSLog(@"成功");
}else if ([dic[@"type"] isEqualToString:@"gxb_error"]) {
NSLog(@"失败");
}
}
重要通知: iOS11以下版本的WKWebView不支持POST请求的发送。
之前不知道这个问题,直到有一天给后台post参数的是发现有的手机不成功。经过各种资料各种查之后,发现是iOS11以下版本的WKWebView不支持POST请求的发送。
可以加个判断:@available(iOS 11.0, *)