美文网首页iOS学习专题ios奇门杂招面试题库
UITableView嵌套WKWebView的那些坑

UITableView嵌套WKWebView的那些坑

作者: 指尖三千卡 | 来源:发表于2017-07-03 10:14 被阅读8914次

    最近项目中遇到了一个需求,TableView中需要嵌套Web页面,我的解决办法是在系统的UITableViewCell中添加WKWebView。开发的过程中,遇到了些坑,写出来分享一下。

    1.首先说一下WKWebView的代理方法中,页面加载完成后会走的代理方法,与UIWebView的页面加载完成代理方法一样。
    - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
    - (void)webViewDidFinishLoad:(UIWebView *)webView;
    这两个方法都是在web页面加载完成后才会走的代理方法。什么才算是加载完成呢?web页面中的所有元素都加载成功,包括图片、音频和视频等资源,都加载成功了,才算加载完成。如果通过这两个代理方法来计算cell的高度,你的整个tableview页面就会加载得很慢,至少要等web页面加载完成后才能计算高度重铺tableview。

    2.WKWebView加载完成展示页面的时候,会让整个web页面自动适应屏幕的宽度。这样展示出来的效果就会很紧凑,页面内容会很小。


    图2.1

    为了让web页面中的元素都适应屏幕的宽度显示,需要对WKWebView进行一下设置,原理是加载js代码,然后通过js来设置webview中的元素大小:

    WKWebViewConfiguration *webConfig = [[WKWebViewConfiguration alloc] init];
    WKUserContentController *wkController = [[WKUserContentController alloc] init];
    webConfig.userContentController = wkController;
    // 自适应屏幕宽度js
    NSString *jsStr = @"var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);";
    WKUserScript *wkScript = [[WKUserScript alloc] initWithSource:jsStr injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
    // 添加js调用
    [wkUController addUserScript:wkScript];
    WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:webConfig];
    
    图2.2

    如果加载的是HTML代码,图片的自适应可以通过下面一段CSS代码来设置:

    NSString *html = @"HTML代码";
    NSString *cssStr = @"<style type=\"text/css\">p{line-height: 24px!important;background-color: #fff;}img{max-width: 100%!important;}</style>";
    NSString *htmlStr = [NSString stringWithFormat:@"<!DOCTYPE html><html><head><title>webview</title></head><body>%@%@</body></html>", cssStr, html];
    [webView loadHTMLString:htmlStr baseURL:nil];
    

    3.接下来说说重头戏,UITableViewCell嵌套WKWebView。

    (1)cell的高度自适应(笔者用的是系统的cell,UITableViewCell)
    cell的高度当然是根据webview的高度来设置的,所以首先要算出webview页面的高度。开篇就已经说过,webview加载完成的代理方法是web页面中的所有元素都加载成功,包括图片、音频和视频等资源,都加载成功了,才算加载完成。如果页面中的元素过多,网络图片过大,视频过大等,就会导致web页面加载卡顿。本身WKWebView这些资源是异步加载的,但是计算cell高度的时候是在这些资源都加载完成后才计算高度,WKWebView也就失去的异步加载的意义,所以整个tableview都会加载得很慢。
    明白了这个道理,我个人推荐使用KVO来代替webview的代理方法。

    // 对webView中的scrollView设置KVO
    [_webView.scrollView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil];
    
    // KVO具体实现
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
    {
        if ([keyPath isEqualToString:@"contentSize"]) {
    
            // 这里有两种方法获得webview高度,第一种就是通过JS代码来计算出高度,如下
            // document.documentElement.scrollHeight
            // document.body.offsetHeight
            /*
            [_webView evaluateJavaScript:@"document.body.offsetHeight" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
                // 加20像素是为了预留出边缘,这里可以随意
                CGFloat height = [result doubleValue] + 20;
                // 设置一个高度属性,赋值后便于设置cell的高度
                _webHeight = height;
                // 设置cell上子视图的frame,主要是高度
                _webView.frame = CGRectMake(0, 0, kScreenWidth, height);
                _scrollView.frame = CGRectMake(0, 0, kScreenWidth, height);
                _scrollView.contentSize =CGSizeMake(kScreenWidth, height);
                // 获取了高度之后,要更新webview所在的cell,其他的cell就不用更新了,这样能更节省资源
                [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects:[NSIndexPath indexPathForRow:1 inSection:0], nil] withRowAnimation:UITableViewRowAnimationNone];
            }];
            */
    
            // 第二种方法就是直接使用监听的contentSize.height(这里要感谢[markss](http://www.jianshu.com/u/cc0bd919cb50)简友的评论提醒),但是与第一种方法不同的就是不能再加多余的高度了,那样会循环出发KVO,不断的增加高度直到crash。
            UIScrollView *scrollView = (UIScrollView *)object;
            CGFloat height = scrollView.contentSize.height;
            _webHeight = height;
            _webView.frame = CGRectMake(0, 0, kScreenWidth, height);
            _scrollView.frame = CGRectMake(0, 0, kScreenWidth, height);
            _scrollView.contentSize =CGSizeMake(kScreenWidth, height);
            [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects:[NSIndexPath indexPathForRow:1 inSection:0], nil] withRowAnimation:UITableViewRowAnimationNone];
        }
    }
    
    // 别忘注销kvo
    - (void)dealloc
    {
        [_webView.scrollView removeObserver:self forKeyPath:@"contentSize"];
    }
    

    这样设置后的webView直接addSubview到webCell.contentView上,页面是加载出来了,但是webView的高度并不能改变。

    (2)然后笔者百度了一下,找到了一个方法。就是先将webView addSubview到一个scrollView上

    [_scrollView addSubview:_webView];
    

    然后再将这个scrollView addSubview到webCell.contentView上

    [webCell.contentView addSubview:_scrollView];
    

    这样webview的高度就可以改变了。但是这是什么原理,笔者还没搞清楚,希望懂的大神能给予热情而细心的指导。

    (3)在刷新tableview刷新cell的时候,会重新走一遍所有的tableview代理方法,所以笔者建议尽量优化- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath中的代码,防止多次加载webview

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        switch (indexPath.row) {
            case 0:
            {
                ArticleHeaderTableViewCell *articleHeaderCell = [tableView dequeueReusableCellWithIdentifier:_articleHeaderIdentifier forIndexPath:indexPath];
                return articleHeaderCell;
            }
                break;
            case 1:
            {
                // 嵌套webivew的cell
                UITableViewCell *webCell = [tableView dequeueReusableCellWithIdentifier:_articleWebIdentifier forIndexPath:indexPath];
                [webCell.contentView addSubview:_scrollView];
                return webCell;
            }
                break;
            case 2:
            {
                ArticleCommentTableViewCell *articleCommentCell = [tableView dequeueReusableCellWithIdentifier:_articleCommentIdentifier forIndexPath:indexPath];
                return articleCommentCell;
            }
                break;
            default:
            {
                CommentsTableViewCell *commentsCell = [tableView dequeueReusableCellWithIdentifier:_commentsIdentifier forIndexPath:indexPath];
                return commentsCell;
            }
                break;
        }
    }
    

    总结:
    1.在cell上嵌套webview之前,一定要在中间多加一层scrollview。
    2.在计算cell高度的时候,不建议直接使用系统webview的代理方法,建议使用kvo。
    3.进行减少- (UITableViewCell )tableView:(UITableView )tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath方法中对webview的操作,这样能减少webview的重复加载,提高性能和速度。

    ps:每个人都有自己的想法,每个人的优化方式不同,笔者也是在摸索中尝试着,如果有说得不对不准确的地方,还请各位指正出来,大家共同探讨,互相学习。

    代码下载地址:https://github.com/VonKeYuan/WKWebViewTest

    相关文章

      网友评论

      • 6239961ce796:这样网页没法交互了,第一事件响应者也没法修改
      • Beyond无状态:楼主 我用了 可是其他cell里的控件会不停创建 怎么解决啊
      • nenhall:如你總結中的第一點,為何要加scrollview,wekview本身不就是繼承自它?
      • HEYRIX:发现一个问题。这么处理之后,在WebView中无法激活输入框了。
        HEYRIX:看了下,是因为KVO太过于频繁了。机器发热也跟这个有关系。
      • 布鲁克先生:首先点个赞👍 我在想能怎么封装一下呢,我多个界面都要这样操作??想请教一下有好的思路嘛?
        指尖三千卡:@布鲁克先生 客气了,其实你发的问题楼上也发现了,我试了一下,也没找到原因,只能用webview的代理和if判断终止kvo,但是具体的原因还是没找到
        布鲁克先生:@天蝎座的Von动必晒 :谢谢楼主,我会试试的 ,我还发现一个问题,我加载的是图文的HTML,如果其中包含了GIF的图片,就会一直调用KVO,一直刷新cell,高度也会出现问题,我也不知道为什么
        指尖三千卡:谢谢层主的支持,我觉着可以把整个webview封装,暴露出url即可,其他页面如果加载webview直接调用封装的view,然后设置url即可
      • 70d71eea9f62:楼主:换个url (例如:http://news.ifeng.com/a/20170721/51471673_0.shtml),无法获取内容,高度也不对。KVO的方法二,是个死循环
        70d71eea9f62:@天蝎座的Von动必晒 哈哈,好的,我一时也没有想起,这个只是在demo里写的,但是用WKWebView 的两个代理方法的加载,内容是可以出来的
        指尖三千卡:其实分析下来也不一定是wk的问题,现在iOS项目中,如果涉及到http访问的,都需要设置这个:blush:
        指尖三千卡:感谢层主的提问,让我又发现了一个wk的坑,wk加载url的时候,需要在info.plist文件中,设置支持http访问,代码如下:
        <key>NSAppTransportSecurity</key>
        <dict>
        <key>NSAllowsArbitraryLoads</key>
        <true/>
        </dict>
        这样就可以正常访问了。
      • 92cffb5e6d5e:问一下 我是图片和文字都有 就只是图片自适应了 文字变的很小
        指尖三千卡:@守护神 好的,我加你了
        92cffb5e6d5e:@天蝎座的Von动必晒 方便加个qq吗?834206460:smile::smile:
        指尖三千卡:层主你是把JS和CSS都加载了吗?我都加载了,而且也是图片和文字都有,没遇到你说的这种情况。
      • Lol刀妹:我之前使用KVO让WKWebView高度自适应内容,有时候获取到的高度远大于实际内容高度,不知道楼主有没有遇到过这种情况。
        指尖三千卡:@无夜之星辰 嗯,是的,在wk创建的时候,通过WKUserScript注入JS代码
        NSString *jsStr = @"var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);";
        Lol刀妹:@天蝎座的Von动必晒 针对这种情况楼主是自己注入js调整元素吗?😄
        指尖三千卡:层主说的这个情况我遇到过,但是不知道是不是同样的问题。当时我的web页面里面的元素没有进行屏幕适应,WKWebView自动将web中的元素进行缩小显示,显示得特别小,例如只占了300px的高度就够了,而实际的web高度是1000px的高度。
      • markss:直接在 if ([keyPath isEqualToString:@"contentSize"]) 这里获取 contentSize 的高度就行,为啥还要调用 js?
        指尖三千卡:嗯,确实可以,受教了。有点小改动的就是不能加多余的像素了,因为会循环触发KVO,不断的增加高度。
      • zl520k:如果加载过,就不用加载,就要使用缓存,缓存网页和高度,但是网页如果变化,就不要缓存了,可以设置一个时间,减少刷新的频率。
        指尖三千卡:嗯,是这样的,wkwebview有缓存功能,所以我们缓存高度就行
      • f7139db11dcd:你好。请问,可以分享一个demo 吗 ?234255235@qq.com 谢谢。
        指尖三千卡:https://github.com/VonKeYuan/WKWebViewTest 这个是下载地址,不客气,谢谢支持

      本文标题:UITableView嵌套WKWebView的那些坑

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