2020年4月起,AppStore 将不再接受使用 UIWebView 的新应用程序
2020年12月起,AppStore不再接受使用 UIWebView 的应用程序更新。
初识 WKWebView
之前汇总过过关于 WKWebView 的相关内容,但再回首还是觉得有好多地方写得不清不楚,所以再细说一下。
细说之前解决两个问题
- 写一个实现 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
- 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];
}