美文网首页iOS tips
UIWebView基础分析

UIWebView基础分析

作者: 撒旦的报复 | 来源:发表于2017-02-26 23:25 被阅读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:中去截图。有三个条件需要判断:

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

    相关文章

      网友评论

        本文标题:UIWebView基础分析

        本文链接:https://www.haomeiwen.com/subject/pnmawttx.html