Latest 0.2.0
Homepage https://github.com/DoTalkLily/LYWebViewController
License MIT
Platforms ios 8.0, requires ARC
Dependencies MJRefresh
Frameworks UIKit, Foundation, WebKit
Authors

基于WKWebView和UIWebView实现的仿微信WebView功能的页面加载库

我们知道比起原生开发,H5有良好的跨平台性(很好地节约人力成本),升级灵活迅速,非常适合产品功能迭代频繁的业务模块,方便实现定制化的页面(千人千面),可以用来外投引流等优点,但是H5页面打开依赖原生的webview作为承载,目前多数产品基于UIWebView打开网页,没有有好的进度条提示,并且没有导航功能(目前在各自的项目中封装个UIViewController实现简单的“返回”按钮),页面加载完成会嘭地出现,尤其在图片、样式和脚本比较多的页面,用户体验上有较大提升空间。介于以上问题,提供一个功能更加完善的webview库使页面展示和浏览器相关操作上能对用户更加友好,同时能允许前端同学更加灵活定制样式和导航等功能。

Usage

在Podfile中加一行:

 target 'MyApp' do
    pod 'LYWebViewController', '~> 0.2'
 -end

然后pod install

目前业界主要的三种打开网页的方式:

  • UIWebView
  • WKWebView (entered on iOS 8)
  • SFSafariViewController (entered on iOS 9)

三者对比如下表所示:

图2 三者功能对比

目前业界比较推荐的做法是用WKWebView,但WKWebView在使用过程中会有很多坑,于是在尽量patch这些坑的前提下,实现UI和手势自定义,同时为了兼容iOS 8以下的应用,选择同时基于UIWebView和WKWebView封装实现以上功能的webview库,以便项目中方便根据使用场景进行选择。

设计与实现

下面我将逐一介绍目前实现的功能:

1 . 支持UIWebView和WKWebView

可以依据业务场景选择使用哪种,使用方式如下:
创建一个基于WKWebView实现的webviewcontroller:

LYWebViewController *webVC = [[LYWKWebViewController alloc] initWithAddress:@"https://github.com/DoTalkLily/LYWebViewController"];
webVC.showsToolBar = NO;
webVC.showsBackgroundLabel = NO;
[self.navigationController pushViewController:webVC animated:YES];

创建一个基于UIWebView实现的webviewcontroller:

LYWebViewController *webVC = [[LYUIWebViewController alloc] initWithAddress:@"https://github.com/DoTalkLily/LYWebViewController"];
webVC.showsToolBar = NO;
webVC.navigationType = LYWebViewControllerNavigationBarItem;
[self.navigationController pushViewController:webVC animated:YES];

2. 页面加载进度条

WKWebView 提供一个estimatedProgress属性代表页面加载进度,可以通过KVO方式监听这个属性来更新进度条。

[self.webView removeObserver:self forKeyPath:@"estimatedProgress"];

UIWebView没有提供可以监听加载进度的属性,因此普遍的实现方式是fake一个进度条,即先慢慢滑到90%,然后等待加载完毕,完毕后瞬间进度到100%,为了方便实现和体验更好一些,可以用NJKWebViewProgress库,原理上也是fake进度进度条的方式。弱网环境下在微信中打开文章,通常是一个白屏但是进度条在推进,最后到80-90%左右卡主如果页面能打卡瞬间滑到100%,如果最终请求超时进度条也不动了,推测也是通过fake实现的。为了减少依赖库,LYWebViewController采用上述思路自己实现。两者效果如下:

WKWebView:

UIWebView:

3. 顶部导航(类似微信的返回、关闭等)& 底部toolbar(仿浏览器)

原生的UIWebView和WKWebView没有提供导航功能,但是提供了判断是否可以前进后退的函数来封装这些功能。LYWebViewController通过增加导航按钮根据webview提供的canGoBack、goBack、canGoForward、goForward等函数实现顶部导航和底部toolbar导航,同时暴露钩子函数给使用者添加相应逻辑。

- (void)goBackClicked:(UIBarButtonItem *)sender
{
    if (self.delegate && [self.delegate respondsToSelector:@selector(willGoBack)]) {
        [self.delegate willGoBack];
    }
    if ([self.webView canGoBack]) {
        [self.webView goBack];
    }
}
- (void)goForwardClicked:(UIBarButtonItem *)sender
{
    if (self.delegate && [self.delegate respondsToSelector:@selector(willGoForward)]) {
        [self.delegate willGoForward];
    }
    if ([self.webView canGoForward]) {
        [self.webView goForward];
    }
}
- (void)reloadClicked:(UIBarButtonItem *)sender
{
    if (self.delegate && [self.delegate respondsToSelector:@selector(willReload)]) {
        [self.delegate willReload];
    }
    [self.webView reload];
}
- (void)stopClicked:(UIBarButtonItem *)sender
{
    if (self.delegate && [self.delegate respondsToSelector:@selector(willStop)]) {
        [self.delegate willStop];
    }
    [self.webView stopLoading];
}

- (void)navigationItemHandleBack:(UIBarButtonItem *)sender
{
    if ([self.webView canGoBack]) {
        [self.webView goBack];
        return;
    }
    [self.navigationController popViewControllerAnimated:YES];
}

效果如下:

4.支持滑动导航

WKWebView 通过设置allowsBackForwardNavigationGestures属性可以实现右滑回退的功能,非常方便,但是UIWebView不支持,可以通过维护各网页的快照数组结合滑动手势来实现。
具体实现参见这里

UIWebView+右滑回退效果如下:

5. 支持唤起appstore下载

通常广告主会提供一个带各种下载按钮的H5页面,链接到“https://itunes.apple.com/” 开头的自己应用的页面,但原生WK和UIWebView不支持跳转到appstore,因此在UIWebView提供的“webView: shouldStartLoadWithRequest:navigationType:” 中解析拦截到的url,处理特殊跳转逻辑。(WKWebView是webView:decidePolicyForNavigationAction:decisionHandler:)

#pragma mark - UIWebViewDelegate
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    if ([request.URL.absoluteString isEqualToString:kLY404NotFoundURLKey] ||
        [request.URL.absoluteString isEqualToString:kLYNetworkErrorURLKey]) {
        [self loadURL:self.URL];
        return NO;
    }

    NSURLComponents *components = [[NSURLComponents alloc] initWithString:request.URL.absoluteString];
    // For appstore.
    if ([[NSPredicate predicateWithFormat:@"SELF BEGINSWITH[cd] 'https://itunes.apple.com/' OR SELF BEGINSWITH[cd] 'mailto:' OR SELF BEGINSWITH[cd] 'tel:' OR SELF BEGINSWITH[cd] 'telprompt:'"] evaluateWithObject:request.URL.absoluteString]) {
        if ([[NSPredicate predicateWithFormat:@"SELF BEGINSWITH[cd] 'https://itunes.apple.com/'"] evaluateWithObject:components.URL.absoluteString] && !self.reviewsAppInAppStore) {
            SKStoreProductViewController *productVC = [[SKStoreProductViewController alloc] init];
            productVC.delegate = self;
            NSError *error;
            NSRegularExpression *regex = [[NSRegularExpression alloc] initWithPattern:@"id[1-9]\d*" options:NSRegularExpressionCaseInsensitive error:&error];
            NSTextCheckingResult *result = [regex firstMatchInString:components.URL.absoluteString options:NSMatchingReportCompletion range:NSMakeRange(0, components.URL.absoluteString.length)];

            if (!error && result) {
                NSRange range = NSMakeRange(result.range.location+2, result.range.length-2);
                [productVC loadProductWithParameters:@{SKStoreProductParameterITunesItemIdentifier: @([[components.URL.absoluteString substringWithRange:range] integerValue])} completionBlock:^(BOOL result, NSError * _Nullable error) {
                }];
                [self presentViewController:productVC animated:YES completion:NULL];
                return NO;
            }
        }
        if ([[UIApplication sharedApplication] canOpenURL:request.URL]) {
            if (UIDevice.currentDevice.systemVersion.floatValue >= 10.0){
                [UIApplication.sharedApplication openURL:request.URL options:@{} completionHandler:NULL];
            } else {
                [[UIApplication sharedApplication] openURL:request.URL];
            }
        }
        return NO;
    } else if (![[NSPredicate predicateWithFormat:@"SELF MATCHES[cd] 'https' OR SELF MATCHES[cd] 'http' OR SELF MATCHES[cd] 'file' OR SELF MATCHES[cd] 'about'"] evaluateWithObject:components.scheme]) {// For any other schema.
        if ([[UIApplication sharedApplication] canOpenURL:request.URL]) {
            if (UIDevice.currentDevice.systemVersion.floatValue >= 10.0) {
                [UIApplication.sharedApplication openURL:request.URL options:@{} completionHandler:NULL];
            } else {
                [[UIApplication sharedApplication] openURL:request.URL];
            }
        }
        return NO;
    }

    if (navigationType == UIWebViewNavigationTypeLinkClicked ||
        navigationType == UIWebViewNavigationTypeFormSubmitted ||
        navigationType == UIWebViewNavigationTypeOther) {
        [self pushCurrentSnapshotViewWithRequest:request];
    }

    if (self.navigationType == LYWebViewControllerNavigationBarItem) {
        [self updateNavigationItems];
    }

    if (self.navigationType == LYWebViewControllerNavigationToolItem) {
        [self updateToolbarItems];
    }
    return YES;
}

可以实现跳转到appstore(手机中的appstore应用)或者在页面内打开appstore,以及打开邮件应用,打电话等。

效果如下:

6.记录上一次浏览位置

微信中打开网页有个很不错的功能,一个很长的网页看到一半关掉,下一次打开会自动跳到上一次看到的位置,非常方便。实现思路:将用户访问的url和每次退出时的页面滚动位置缓存起来,在UIWebView中的页面加载完成回调函数中(webViewDidFinishLoad)根据url查询是否有缓存的位置,有则取出来滚动到相应位置。在UIScrollViewDelegate的scrollViewDidEndDecelerating中实时记录滚动位置更新缓存。
效果如下(录屏软件bug导致导航文字不清晰):

7.下拉刷新

下拉刷新是来自前端的诉求,基于MJRefresh实现,也是LYWebViewController唯一依赖的第三方库,暴露MJRefreshHeader属性,业务方可以根据MJRefresh使用说明设置自定义下拉刷新样式和事件。

除了上述功能,还实现以下功能,不一一赘述:

  1. 国际化(支持英文、简体中文、繁体中文)
  2. 样式兼容iPad,同时针对横竖屏样式调整
  3. preview(>=iOS9)
  4. share页提供用chrome、safari打开网页选项
  5. 提供清缓存接口
  6. 自定义UI(toolbar是否展示、进度条颜色等)

What’s next ?

  • 包含jsbridge;
  • 提供更多自定义UI;
  • 研究如何提升页面加载性能做进一步优化。

致谢

感谢阅读,欢迎提issue和pr~

Latest podspec

{
    "name": "LYWebViewController",
    "version": "0.2.0",
    "summary": "A solution for iOS WebView.",
    "description": "A solution for iOS WebView. It provides JSBridge and off-line resoure support for both UIWebView and WKWebView.",
    "homepage": "https://github.com/DoTalkLily/LYWebViewController",
    "license": "MIT",
    "authors": {
        "DoTalkLily": "[email protected]"
    },
    "source": {
        "git": "https://github.com/DoTalkLily/LYWebViewController.git",
        "tag": "0.2.0"
    },
    "platforms": {
        "ios": "8.0"
    },
    "source_files": "LYWebViewController/Classes/**/*.{h,m}",
    "resource_bundles": {
        "LYWebViewController": [
            "LYWebViewController/Resources/*"
        ]
    },
    "requires_arc": true,
    "frameworks": [
        "UIKit",
        "Foundation",
        "WebKit"
    ],
    "public_header_files": "LYWebViewController/Classes/**/*.h",
    "dependencies": {
        "MJRefresh": []
    }
}

Pin It on Pinterest

Share This