iOS tips

UIWebView基础分析

2017-02-26  本文已影响105人  撒旦的报复

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:中去截图。有三个条件需要判断:

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进度条的三种方法

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体系架构

上一篇下一篇

猜你喜欢

热点阅读