UIWebView基础分析

1 引言

根据App Store 审核指南,App浏览网页必须使用WebKit框架。因此在iOS上开发浏览器只能使用UIWebView或者WebKit.framework中的WKWebView(iOS8开始支持)或者SafariServices.framework中的SFSafariViewController(iOS9开始支持),本篇文章中主要分析UIWebView,后续文章中再分析其他两项。

2 UIWebView.h文件分析

UIWebView.h中的代码很少,只有一个Class和一个Protocol,即UIWebView和UIWebViewDelegate.下文会一一解释其属性和方法。

2.1 UIWebView

NS_CLASS_AVAILABLE_IOS(2_0) __TVOS_PROHIBITED @interface UIWebView : UIView <NSCoding, UIScrollViewDelegate> 

//UIWebView的代理
@property (nullable, nonatomic, assign) id <UIWebViewDelegate> delegate;

//UIWebView的子视图,实际是_UIWebViewScrollView类型
@property (nonatomic, readonly, strong) UIScrollView *scrollView NS_AVAILABLE_IOS(5_0);

//加载网页请求
- (void)loadRequest:(NSURLRequest *)request;

/**加载本地网页
 * @param string HTML内容
 * @param baseURL HTML中有用到相对路径时,需要设置
 */
- (void)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;

/**加载本地网页
 * @param data MIMEType类型的数据
 * @param MIMEType MIME类型
 * @param textEncodingName 编码方式
 * @param baseURL HTML中有用到相对路径时,需要设置
 */
- (void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType textEncodingName:(NSString *)textEncodingName baseURL:(NSURL *)baseURL;

// 当前请求
@property (nullable, nonatomic, readonly, strong) NSURLRequest *request;

//刷新
- (void)reload;
//停止加载
- (void)stopLoading;
//后退
- (void)goBack;
//前进
- (void)goForward;

//能否后退
@property (nonatomic, readonly, getter=canGoBack) BOOL canGoBack;
//能否前进
@property (nonatomic, readonly, getter=canGoForward) BOOL canGoForward;
//加载状态
@property (nonatomic, readonly, getter=isLoading) BOOL loading;

/** JS注入
 * @param script JavaScript脚本
 * @return script脚本执行的返回值
 */
- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;

//自动缩放页面以适应屏幕
@property (nonatomic) BOOL scalesPageToFit;

//数据检测类型,系统自动将相应类型的内容转换为可点击的URL,默认是UIDataDetectorTypePhoneNumber
@property (nonatomic) UIDataDetectorTypes dataDetectorTypes NS_AVAILABLE_IOS(3_0);

//允许在网页内播放媒体(iPhone上默认是NO,会打开全屏播放,iPad默认是YES,在网页内播放)
@property (nonatomic) BOOL allowsInlineMediaPlayback NS_AVAILABLE_IOS(4_0); // iPhone Safari defaults to NO. iPad Safari defaults to YES

//HTML5视频点击播放还是自动播放
@property (nonatomic) BOOL mediaPlaybackRequiresUserAction NS_AVAILABLE_IOS(4_0); // iPhone and iPad Safari both default to YES

//是否允许Air Play
@property (nonatomic) BOOL mediaPlaybackAllowsAirPlay NS_AVAILABLE_IOS(5_0); // iPhone and iPad Safari both default to YES

//是否阻止增量渲染
@property (nonatomic) BOOL suppressesIncrementalRendering NS_AVAILABLE_IOS(6_0); // iPhone and iPad Safari both default to NO

//是否允许网页内容通过代码打开键盘
@property (nonatomic) BOOL keyboardDisplayRequiresUserAction NS_AVAILABLE_IOS(6_0); // default is YES

//分页模式,即改变网页内容的布局方式,网页内容被拆成若干页显示。默认是UIWebPaginationModeUnpaginated不分页
@property (nonatomic) UIWebPaginationMode paginationMode NS_AVAILABLE_IOS(7_0);

//断页模式
@property (nonatomic) UIWebPaginationBreakingMode paginationBreakingMode NS_AVAILABLE_IOS(7_0);

//单个page的长度
@property (nonatomic) CGFloat pageLength NS_AVAILABLE_IOS(7_0);

//page之间的空隙
@property (nonatomic) CGFloat gapBetweenPages NS_AVAILABLE_IOS(7_0);

//page的数量
@property (nonatomic, readonly) NSUInteger pageCount NS_AVAILABLE_IOS(7_0);

//是否允许画中画媒体播放
@property (nonatomic) BOOL allowsPictureInPictureMediaPlayback NS_AVAILABLE_IOS(9_0);

//允许链接预览(即3DTouch操作),pop操作会打开Safari
@property (nonatomic) BOOL allowsLinkPreview NS_AVAILABLE_IOS(9_0); // default is NO
@end

2.2 UIWebViewDelegate

__TVOS_PROHIBITED @protocol UIWebViewDelegate <NSObject>

@optional
/**
 * 决定webView是否加载一个frame
 * @param webview 
 * @param request 待加载的请求
 * @param navigationType 加载类型
 * @return 是否加载request
 */
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;

//webView开始加载一个frame
- (void)webViewDidStartLoad:(UIWebView *)webView;
//webView完成加载一个frame
- (void)webViewDidFinishLoad:(UIWebView *)webView;

//webView加载frame时发生错误
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error;

@end

官方文档给出的提示要求,在销毁UIWebView之前应该设置其delegate为nil。

3 UIWebView体验优化

UIWebView的体验与Safari相比,并不太好,可以对其进行优化

3.1 支持滑动返回

WKWebView和SFSafariViewController都支持滑动返回,但是UIWebView并不支持。要使得UIWebView支持滑动返回的方法至少有两种,包括截图的方式,以及使用多个控制器加载UIWebView的方式。

WKWebView和Safari应该都是用的截图方式来实现滑动返回,QQ浏览器个人猜测是使用的多个子控制器(或者多个UIWebView)来实现。这两种方法相比,截图方式比较节省资源,使用多个控制器实现比较简单。下面介绍采用截图方式的实现的方法。

3.1.1 解决与系统侧滑手势冲突

首先给webView添加UIScreenEdgePanGesture

[self.webView addGestureRecognizer:self.screenEdgePanGesture];

- (UIScreenEdgePanGestureRecognizer *)screenEdgePanGesture
{
    if(!_screenEdgePanGesture)
    {
        _screenEdgePanGesture = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(screenEdgePanned:)];
        _screenEdgePanGesture.edges = UIRectEdgeLeft;
        _screenEdgePanGesture.delegate = self;
    }
    return _screenEdgePanGesture;
}

如果要自定义返回按钮的话实现UIGestureRecognizerDelegate协议,避免与系统滑动返回手势冲突

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    if(self.navigationController)
    {
        self.nav = self.navigationController;
        self.nav.interactivePopGestureRecognizer.delegate = self;
    }
}
- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];
    if(self.nav)
    {
        self.nav.interactivePopGestureRecognizer.delegate = nil;
    }
}
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    //无法后退时,返回NO,系统右滑手势可成功识别
    if(gestureRecognizer == self.screenEdgePanGesture && !self.webView.canGoBack)
    {
        return NO;
    }
    return YES;
}

有一种方法可以不必考虑与系统返回的冲突,就是在不能goBack时使用系统默认的返回按钮,在可以goBack时使用自定义按钮

self.navigationItem.leftBarButtonItems = self.webView.canGoBack ? @[self.backButtonItem,self.closeButtonItem] : nil;

3.1.2 截图时机

webView:shouldStartLoadWithRequest:navigationType:中去截图。有三个条件需要判断:

  • 是否是mainFrame,[request.mainDocumentURL isEqual:request.URL];
  • 并判断navigationType,后退和刷新不需要截图,
  • 页内跳转不截图

3.1.3 滑动处理

在screenEdgePan手势识别成功时,将相应截图视图插入在当前视图下方,在动画完成时,将截图视图移除。其实就是模拟系统滑动返回的过程。

3.1.4 设置PageCache

由于UIWebView默认没有PageCache,返回到上一页,页面会刷新,并不能停留在之前浏览的位置(除非前端有处理),使用以下代码可以设置PageCache,可以后退不刷新。详细解释见解决UIWebView 前进、后退刷新的坑

//私有方法,请慎用
((void *(*)(id,SEL,...))objc_msgSend)(NSClassFromString(@"WebView"),NSSelectorFromString(@"_setCacheModel:"),2);

3.2 进度条

这边介绍UIWebView进度条的三种方法

  • 使用假的进度,进度条加载到一定进度时,则不更新进度,直到完成时继续加载。
  • 使用NJKWebViewProgress,就四个文件,集成很简单,看懂了也可以自己写。
  • 监听私有通知WebProgressEstimatedProgressKey,在回调的notification的userInfo里有一个KeyWebProgressEstimatedProgressKey对应的value即为进度(P.S. 可能它并不是针对从开始请求到加载完成过程的估算,所以这个进度不是很准,这种东西也没发精准,不是么?而且是私有通知,请慎用)

3.3 标题

获取网页标题的方法很简单,执行代码NSString *title = [webview stringByEvaluatingJavaScriptFromString:@"document.title"];即可。

不过有时在webViewDidFinishLoad:中获取不到当前title,因为前端可能用AJAX去请求数据再修改title,而这个过程原生是捕获不到的。所以个人想了个有点复杂的方法,就是获取documentView.webView.mainFrame.javaScriptContext,监听readystatechangeDOMSubtreeModified事件。

- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    [webView stringByEvaluatingJavaScriptFromString:@"document.documentElement.style.webkitTouchCallout = 'none';"];
    self.context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    __weak typeof(self)weakSelf = self;
    self.context[@"OCDocumentReady"] = ^(){
        [weakSelf onDocumentReady];
    };
    self.context[@"OCDocumentChange"] = ^(){
        [weakSelf onDocumentChange];
    };
    if([[self.context evaluateScript:@"document.readyState == 'complete'"] toBool])
    {
        [self onDocumentReady];
    }
    else
    {
        [self.context evaluateScript:@"document.addEventListener('readystatechange',function(){if(document.readyState == 'complete'){OCDocumentReady();}},false)"];
    }
}

- (void)onDocumentChange
{
    NSString *host = [[self.context evaluateScript:@"location.host"] toString];
    NSString *title = [[self.context evaluateScript:@"document.title"] toString];
    self.title = title.length ? title : host;
        
    self.navigationItem.leftBarButtonItems = self.webView.canGoBack ? @[self.backButtonItem,self.closeButtonItem] : nil;
}
- (void)onDocumentReady
{
    self.loadingFinished = YES;
    [self.context evaluateScript:@"document.documentElement.addEventListener('DOMSubtreeModified', function(e) {OCDocumentChange();}, false);"];
    [self onDocumentChange];
}

这样每次DOM树发生变化时,都会检测title,有些冗余,应该有更好的方法,还请不吝赐教。

如果不了解Objective-C与JavaScript的交互方法的话,可以查看Objective-C与JavaScript交互的那些事

3.4 错误页面

其实很多应用的内置浏览器都没有错误页面,个人觉得还是应该弄一下。需要注意的是mainFrame出错时,才应该显示错误页面。

- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
    if([error.domain isEqual:NSURLErrorDomain] && error.code == NSURLErrorCancelled)
    {//后退或者取消加载,不需要处理
        return ;
    }
    else if([error.userInfo[@"NSErrorFailingURLStringKey"] isEqualToString:self.currentMainDocumentURL])
    {//mainFrame加载出错时显示错误页面,这里currentMainDocumentURL,是在请求时记录的
        //错误处理
    }
}

如果设计的错误页面是网页的话,并且仍然用当前webView加载,则需要先执行[self loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"about:blank"]]];再加载错误页面,这样后退时才能回到上一个页面,因为UIWebView加载本地网页时,会把上一个页面覆盖掉。

4 坑

个人发现UIWebView有一个BUG,当在页面A执行location.replace(B),再从B页面跳转到C,这时再返回,居然是到页面A。。。(location.replace(B)的意思是用B页面取代A页面,正确结果应该回到B,A页面就不应该存在于浏览队列中)。

UIWebView还有各种莫名的Bug,还是使用WKWebView吧,上述优化功能都自带了,而且体验好很多。

5 写在最后

第一次在简书上发文章,才疏学浅,如果发现有错误,烦请指出。
如果要更深入理解UIWebView,可以查看
UIWebView体系架构

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,245评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,749评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,960评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,575评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,668评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,670评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,664评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,422评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,864评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,178评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,340评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,015评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,646评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,265评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,494评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,261评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,206评论 2 352

推荐阅读更多精彩内容