前言
一直想系统的总结下UIWebView
和WKWebView
,这里整理了一个
Demo可供参考
分为两部分:
UIWebView & WKWebView 上
UIWebView & WKWebView 下
简介
WKWebView
是Apple于iOS 8.0推出的WebKit
中的核心控件,用来替代UIWebView
。WKWebView
比UIWebView
的优势在于:
- 更多的支持HTML5的特性
- 高达60fps的滚动刷新率以及内置手势
- 与Safari相同的JavaScript引擎
- 将UIWebViewDelegate与UIWebView拆分成了14类与3个协议(官方文档说明)
- 可以获取加载进度:
estimatedProgress
(UIWebView需要调用私有Api)
POST请求
WKWebView相关的post请求实现
Html实现
OC中代码实现:
思路:
1 、将一个包含JavaScript的POST请求的HTML代码放到工程目录中
2 、加载这个包含JavaScript的POST请求的代码到WKWebView
3 、加载完成之后,用Native调用JavaScript的POST方法并传入参数来完成请求
//仅当第一次的时候加载本地JS
// @property(nonatomic,assign) BOOL needLoadJSPOST;
- (void)viewDidLoad
{
// JS发送POST的Flag,为真的时候会调用JS的POST方法
self.needLoadJSPOST = YES;
//POST使用预先加载本地JS方法的html实现,请确认WKJSPOST存在
[self loadHostPathURL:@"WKJSPOST"];
}
- (void)loadHostPathURL:(NSString *)url
{
//获取JS所在的路径
NSString *path = [[NSBundle mainBundle] pathForResource:url ofType:@"html"];
//获得html内容
NSString *html = [[NSString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
//加载js
[self.webView loadHTMLString:html baseURL:[[NSBundle mainBundle] bundleURL]];
}
//加载成功,对应UIWebView的- (void)webViewDidFinishLoad:(UIWebView *)webView; 网页加载完成,导航的变化
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
/*
主意:这个方法是当网页的内容全部显示(网页内的所有图片必须都正常显示)的时候调用(不是出现的时候就调用),,否则不显示,或则部分显示时这个方法就不调用。
*/
// 判断是否需要加载(仅在第一次加载)
if (self.needLoadJSPOST) {
// 调用使用JS发送POST请求的方法
[self postRequestWithJS];
// 将Flag置为NO(后面就不需要加载了)
self.needLoadJSPOST = NO;
}
}
#pragma mark - JSPOST
// 调用JS发送POST请求
- (void)postRequestWithJS
{
// 拼装成调用JavaScript的字符串
NSString *jscript = [NSString stringWithFormat:@"post('%@',{%@})",self.URLString,self.postData];
NSLog(@"Javascript: %@", jscript);
//post('http://www.postexample.com',{"username":"aaa","password":"123"})
// 调用JS代码
[self.webView evaluateJavaScript:jscript completionHandler:^(id object, NSError * _Nullable error) {
NSLog(@"%@",error);
}];
}
UIWebView的post请求实现
- (void)viewDidLoad {
[super viewDidLoad];
NSURL*url=[NSURL URLWithString:@"http://your_url.com"];
NSString*body=[NSString stringWithFormat:@"arg1=%@&arg2=%@",@"val1",@"val2"];
NSMutableURLRequest*request=[[NSMutableURLRequest alloc]initWithURL:url];
[request setHTTPMethod:@"POST"];
[request setHTTPBody:[body dataUsingEncoding:NSUTF8StringEncoding]];
[self.webView loadRequest:request];
}
Cookie相关
WKWebView的cookie管理
比起UIWebView
的自动管理,WKWebView
的Cookie
管理坑还是比较多的,注意事项如下:
1、WKWebView
加载网页得到的Cookie
会同步到NSHTTPCookieStorage
中
2、WKWebView加载请求时,不会同步NSHTTPCookieStorage
中已有的Cookie
3、通过共用一个WKProcessPool
并不能解决2中Cookie同步问题,且可能会造成Cookie丢失。
添加cookie
动态注入js
WKUserContentController *UserContentController = [[WKUserContentController alloc] init];
//添加自定义的cookie
WKUserScript *newCookieScript = [[WKUserScript alloc] initWithSource:@"document.cookie = 'SyhCookie=Syh;'" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
//添加脚本
[UserContentController addUserScript:newCookieScript];
解决后续Ajax请求Cookie丢失问题
添加WKUserScript
,需保证sharedHTTPCookieStorage
中你的Cookie存在。
/*!
* 更新webView的cookie
*/
- (void)updateWebViewCookie
{
WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource:[self cookieString] injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
//添加Cookie
[self.configuration.userContentController addUserScript:cookieScript];
}
- (NSString *)cookieString
{
NSMutableString *script = [NSMutableString string];
[script appendString:@"var cookieNames = document.cookie.split('; ').map(function(cookie) { return cookie.split('=')[0] } );\n"];
for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
// Skip cookies that will break our script
if ([cookie.value rangeOfString:@"'"].location != NSNotFound) {
continue;
}
// Create a line that appends this cookie to the web view's document's cookies
[script appendFormat:@"if (cookieNames.indexOf('%@') == -1) { document.cookie='%@'; };\n", cookie.name, cookie.da_javascriptString];
}
return script;
}
@interface NSHTTPCookie (Utils)
- (NSString *)da_javascriptString;
@end
@implementation NSHTTPCookie (Utils)
- (NSString *)da_javascriptString
{
NSString *string = [NSString stringWithFormat:@"%@=%@;domain=%@;path=%@",
self.name,
self.value,
self.domain,
self.path ?: @"/"];
if (self.secure) {
string = [string stringByAppendingString:@";secure=true"];
}
return string;
}
@end
解决跳转新页面时Cookie带不过去问题
当你点击页面上的某个链接,跳转到新的页面,Cookie又丢了,需保证sharedHTTPCookieStorage
中你的Cookie存在。
//核心方法:
/**
修复打开链接Cookie丢失问题
@param request 请求
@return 一个fixedRequest
*/
- (NSURLRequest *)fixRequest:(NSURLRequest *)request
{
NSMutableURLRequest *fixedRequest;
if ([request isKindOfClass:[NSMutableURLRequest class]]) {
fixedRequest = (NSMutableURLRequest *)request;
} else {
fixedRequest = request.mutableCopy;
}
//防止Cookie丢失
NSDictionary *dict = [NSHTTPCookie requestHeaderFieldsWithCookies:[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies];
if (dict.count) {
NSMutableDictionary *mDict = request.allHTTPHeaderFields.mutableCopy;
[mDict setValuesForKeysWithDictionary:dict];
fixedRequest.allHTTPHeaderFields = mDict;
}
return fixedRequest;
}
#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
#warning important 这里很重要
//解决Cookie丢失问题
NSURLRequest *originalRequest = navigationAction.request;
[self fixRequest:originalRequest];
//如果originalRequest就是NSMutableURLRequest, originalRequest中已添加必要的Cookie,可以跳转
//允许跳转
decisionHandler(WKNavigationActionPolicyAllow);
//可能有小伙伴,会说如果originalRequest是NSURLRequest,不可变,那不就添加不了Cookie了,是的,我们不能因为这个问题,不允许跳转,也不能在不允许跳转之后用loadRequest加载fixedRequest,否则会出现死循环,具体的,小伙伴们可以用本地的html测试下。
NSLog(@"%@", NSStringFromSelector(_cmd));
}
#pragma mark - WKUIDelegate
- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {
#warning important 这里也很重要
//这里不打开新窗口
[self.webView loadRequest:[self fixRequest:navigationAction.request]];
return nil;
}
Cookie依然丢失
什么的方法需保证sharedHTTPCookieStorage中你的Cookie存在。怎么保证呢?由于WKWebView
加载网页得到的Cookie
会同步到NSHTTPCookieStorage
中的特点,有时候你强行添加的Cookie
会在同步过程中丢失。抓包(Mac推荐Charles)你就会发现,点击一个链接时,Request
的header
中多了Set-Cookie
字段,其实Cookie已经丢了。下面推荐笔者的解决方案,那就是把自己需要的Cookie
主动保存起来,每次调用[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies
方法时,保证返回的数组中有自己需要的Cookie
。下面上代码,用了runtime
的Method Swizzling
首先是在适当的时候,保存
//比如登录成功,保存Cookie
NSArray *allCookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
for (NSHTTPCookie *cookie in allCookies) {
if ([cookie.name isEqualToString:DAServerSessionCookieName]) {
NSDictionary *dict = [[NSUserDefaults standardUserDefaults] dictionaryForKey:DAUserDefaultsCookieStorageKey];
if (dict) {
NSHTTPCookie *localCookie = [NSHTTPCookie cookieWithProperties:dict];
if (![cookie.value isEqual:localCookie.value]) {
NSLog(@"本地Cookie有更新");
}
}
[[NSUserDefaults standardUserDefaults] setObject:cookie.properties forKey:DAUserDefaultsCookieStorageKey];
[[NSUserDefaults standardUserDefaults] synchronize];
break;
}
}
在读取时,如果没有则添加
@implementation NSHTTPCookieStorage (Utils)
+ (void)load
{
class_methodSwizzling(self, @selector(cookies), @selector(da_cookies));
}
- (NSArray *)da_cookies
{
NSArray *cookies = [self da_cookies];
BOOL isExist = NO;
for (NSHTTPCookie *cookie in cookies) {
if ([cookie.name isEqualToString:DAServerSessionCookieName]) {
isExist = YES;
break;
}
}
if (!isExist) {
//CookieStroage中添加
NSDictionary *dict = [[NSUserDefaults standardUserDefaults] dictionaryForKey:DAUserDefaultsCookieStorageKey];
if (dict) {
NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:dict];
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
NSMutableArray *mCookies = cookies.mutableCopy;
[mCookies addObject:cookie];
cookies = mCookies.copy;
}
}
return cookies;
}
@end
UIWebView的cookie管理
UIWebView
的Cookie
管理很简单,一般不需要我们手动操作Cookie
,因为所有Cookie
都会被[NSHTTPCookieStorage sharedHTTPCookieStorage]
这个单例管理,而且UIWebView
会自动同步CookieStorage
中的Cookie
,所以只要我们在Native端,正常登陆退出,h5在适当时候刷新,就可以正确的维持登录状态,不需要做多余的操作。
可能有一些情况下,我们需要在访问某个链接时,添加一个固定Cookie
用来做区分,那么就可以通过header
来实现
思路:
1、主动操作NSHTTPCookieStorage
,添加一个自定义Cookie
2、读取所有Cookie
3、Cookie
转换成HTTPHeaderFields
,并添加到request的header
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:self.URLString]];
//主动操作NSHTTPCookieStorage,添加一个自定义Cookie
NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:@{
NSHTTPCookieName: @"customCookieName",
NSHTTPCookieValue: @"heiheihei",
NSHTTPCookieDomain: @".baidu.com",
NSHTTPCookiePath: @"/"
}];
//Cookie存在则覆盖,不存在添加
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
//读取所有Cookie
NSArray *cookies = [NSHTTPCookieStorage sharedHTTPCookieStorage].cookies;
//Cookies数组转换为requestHeaderFields
NSDictionary *requestHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
//设置请求头
request.allHTTPHeaderFields = requestHeaderFields;
[self.webView loadRequest:request];
自定义浏览器UserAgen
这个其实在App开发中,比较重要。比如常见的微信、支付宝App等,都有自己的UserAgent
,而UA最常用来判断在哪个App内,一般App的下载页中只有一个按钮"点击下载",当用户点击该按钮时,在微信中则跳转到应用宝,否则跳转到AppStore。那么如何区分在哪个App中呢?就是js判断UA
//js中判断
if (navigator.userAgent.indexOf("MicroMessenger") !== -1) {
//在微信中
}
关于自定义UA,这个UIWebView不提供Api,而WKWebView提供Api,前文中也说明过,就是调用customUserAgent属性。
self.webView.customUserAgent = @"WebViewDemo/1.1.0"; //自定义UA,只支持WKWebView
而有没有其他的方法实现自定义浏览器UserAgent呢?有
//最好在AppDelegate中就提前设置
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
//设置自定义UserAgent
[self setCustomUserAgent];
return YES;
}
- (void)setCustomUserAgent
{
//get the original user-agent of webview
UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectZero];
NSString *oldAgent = [webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];
//add my info to the new agent
NSString *newAgent = [oldAgent stringByAppendingFormat:@" %@", @"WebViewDemo/1.0.0"];
//regist the new agent
NSDictionary *dictionnary = [[NSDictionary alloc] initWithObjectsAndKeys:newAgent, @"UserAgent", newAgent, @"User-Agent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dictionnary];
}
@end
说明:
1、通过NSUserDefaults设置自定义UserAgent,可以同时作用于UIWebView和WKWebView。
2、WKWebView的customUserAgent属性,优先级高于NSUserDefaults,当同时设置时,显示customUserAgent的值。
WKWebView自定义返回/关闭按钮
//返回按钮
@property (nonatomic)UIBarButtonItem* customBackBarItem;
//关闭按钮
@property (nonatomic)UIBarButtonItem* closeButtonItem;
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
[self updateNavigationItems];
//允许跳转
decisionHandler(WKNavigationActionPolicyAllow);
}
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
[self updateNavigationItems];
}
- (void)updateNavigationItems
{
if (self.webView.canGoBack) {
UIBarButtonItem *spaceButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
spaceButtonItem.width = -6.5;
[self.navigationItem setLeftBarButtonItems:@[spaceButtonItem,self.customBackBarItem,self.closeButtonItem] animated:NO];
}else {
self.navigationController.interactivePopGestureRecognizer.enabled = YES;
[self.navigationItem setLeftBarButtonItems:@[self.customBackBarItem]];
}
}
-(UIBarButtonItem*)customBackBarItem{
if (!_customBackBarItem) {
UIImage* backItemImage = [[UIImage imageNamed:@"backItemImage"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
UIImage* backItemHlImage = [[UIImage imageNamed:@"backItemImage-hl"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
UIButton* backButton = [[UIButton alloc] init];
[backButton setTitle:@"返回" forState:UIControlStateNormal];
[backButton setTitleColor:self.navigationController.navigationBar.tintColor forState:UIControlStateNormal];
[backButton setTitleColor:[self.navigationController.navigationBar.tintColor colorWithAlphaComponent:0.5] forState:UIControlStateHighlighted];
[backButton.titleLabel setFont:[UIFont systemFontOfSize:17]];
[backButton setImage:backItemImage forState:UIControlStateNormal];
[backButton setImage:backItemHlImage forState:UIControlStateHighlighted];
[backButton sizeToFit];
[backButton addTarget:self action:@selector(customBackItemClicked) forControlEvents:UIControlEventTouchUpInside];
_customBackBarItem = [[UIBarButtonItem alloc] initWithCustomView:backButton];
}
return _customBackBarItem;
}
-(void)customBackItemClicked{
if (self.webView.goBack) {
[self.webView goBack];
}else{
[self.navigationController popViewControllerAnimated:YES];
}
}
-(UIBarButtonItem*)closeButtonItem{
if (!_closeButtonItem) {
_closeButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"关闭" style:UIBarButtonItemStylePlain target:self action:@selector(closeItemClicked)];
}
return _closeButtonItem;
}
-(void)closeItemClicked{
[self.navigationController popViewControllerAnimated:YES];
}
WKWebView添加进度条
- (void)viewDidLoad
{
//设置加载进度条
//@property (nonatomic,strong) UIProgressView *progressView;
//static void *WkwebBrowserContext = &WkwebBrowserContext;
//添加进度条
[self.view addSubview:self.progressView];
[self.webView addObserver:self forKeyPath:NSStringFromSelector(@selector(estimatedProgress)) options:0 context:WkwebBrowserContext];
}
//开始加载,对应UIWebView的- (void)webViewDidStartLoad:(UIWebView *)webView;
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation
{
//开始加载的时候,让加载进度条显示
self.progressView.hidden = NO;
}
#pragma mark - 进度条
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:NSStringFromSelector(@selector(estimatedProgress))] && object == self.webView) {
self.progressView.alpha = 1.0f;
BOOL animated = self.webView.estimatedProgress > self.progressView.progress;
[self.progressView setProgress:self.webView.estimatedProgress animated:animated];
// Once complete, fade out UIProgressView
if (self.webView.estimatedProgress >= 1.0f) {
[UIView animateWithDuration:0.3f delay:0.3f options:UIViewAnimationOptionCurveEaseOut animations:^{
self.progressView.alpha = 0.0f;
} completion:^(BOOL finished) {
[self.progressView setProgress:0.0f animated:NO];
}];
}
}else{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (UIProgressView *)progressView{
if (!_progressView) {
_progressView = [[UIProgressView alloc]initWithProgressViewStyle:UIProgressViewStyleDefault];
if (_isNavHidden == YES) {
_progressView.frame = CGRectMake(0, 20, self.view.bounds.size.width, 3);
}else{
_progressView.frame = CGRectMake(0, 64, self.view.bounds.size.width, 3);
}
// 设置进度条的色彩
[_progressView setTrackTintColor:[UIColor colorWithRed:240.0/255 green:240.0/255 blue:240.0/255 alpha:1.0]];
_progressView.progressTintColor = [UIColor greenColor];
}
return _progressView;
}
WKWebView填坑
js alert方法不弹窗
实现- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler;
方法,如果不实现,就什么都不发生,好吧,乖乖实现吧,实现了就能弹窗了。
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(nonnull void (^)(void))completionHandler
{
//js 里面的alert实现,如果不实现,网页的alert函数无效
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
completionHandler();
}]];
[self presentViewController:alertController animated:YES completion:^{}];
}
白屏问题
当WKWebView加载的网页占用内存过大时,会出现白屏现象。解决方案是
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView {
[webView reload]; //刷新就好了
}
有时白屏,不会调用该方法,具体的解决方案是
比如,最近遇到在一个高内存消耗的H5页面上 present 系统相机,拍照完毕后返回原来页面的时候出现白屏现象(拍照过程消耗了大量内存,导致内存紧张,WebContent Process 被系统挂起),但上面的回调函数并没有被调用。在WKWebView白屏的时候,另一种现象是 webView.titile 会被置空, 因此,可以在 viewWillAppear 的时候检测 webView.title 是否为空来 reload 页面。(出自WKWebView 那些坑)
自定义contentInset刷新时页面跳动的bug
self.webView.scrollView.contentInset = UIEdgeInsetsMake(64, 0, 49, 0);
//史诗级神坑,为何如此写呢?参考https://opensource.apple.com/source/WebKit2/WebKit2-7600.1.4.11.10/ChangeLog
[self.webView setValue:[NSValue valueWithUIEdgeInsets:self.webView.scrollView.contentInset] forKey:@"_obscuredInsets"]; //kvc给WKWebView的私有变量_obscuredInsets设置值