
概述 本篇一起来学习如何使用UICollectionView来实现水平滚动的缩放式卡片布局,就像Nice App中的卡片布局。 前一篇中讲了如何实现CollectionView旋转水平卡片布局,如果还没有阅读过,不防先看看再继续往下阅读。 实现效果 实现思路 从Demo效果图中,可以看出来,主要是缩放系数的计算。对于不同距离的cell,其缩放系数要变化,以便整体协调显示。 所以,我们必须重写-layoutAttributesForElementsInRect:方法来实现所有当前可见的cell的attributes。 计算比例,通过获取当前偏移rect的最小坐标x,再与atribute的中心x相减,再除以高度,就是高度的缩放倍数scaleForDistance。 最后,通过一个公式来计算缩放的Y系数。公式为: 1 2 3 scale = 1 + scaleFactor * (1 - fabs(scaleForDistance)) scaleFactor因子可以自由调整,值越大,显示就越大。 核心代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #pragma mark - Override - (void)prepareLayout { self.scrollDirection = UICollectionViewScrollDirectionHorizontal; self.minimumLineSpacing = 20; // self.minimumInteritemSpacing = 20; self.sectionInset = UIEdgeInsetsMake(0, 30, 0, 30); self.itemSize = CGSizeMake(self.collectionView.frame.size.width - 60, self.collectionView.frame.size.height - 180); [super prepareLayout]; } - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { return YES; } - (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect { NSArray *superAttributes = [super layoutAttributesForElementsInRect:rect]; NSArray *attributes = [[NSArray alloc] initWithArray:superAttributes copyItems:YES]; CGRect visibleRect = CGRectMake(self.collectionView.contentOffset.x, self.collectionView.contentOffset.y, self.collectionView.frame.size.width, self.collectionView.frame.size.height); CGFloat offset = CGRectGetMidX(visibleRect); [attributes enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *attribute, NSUInteger idx, BOOL * _Nonnull stop) { CGFloat distance = offset - attribute.center.x; // 越往中心移动,值越小,那么缩放就越小,从而显示就越大 // 同样,超过中心后,越往左、右走,缩放就越大,显示就越小 CGFloat scaleForDistance = distance / self.itemSize.height; // 0.2可调整,值越大,显示就越大 CGFloat scaleForCell = 1 + 0.2 * (1 - fabs(scaleForDistance)); // only scale y-axis attribute.transform3D = CATransform3DMakeScale(1, scaleForCell, 1); attribute.zIndex = 1; }]; return attributes; } 实现pagingEnabled的样式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity { BOOL pagingEnabled = NO; if (pagingEnabled) { // 分页以1/3处 if (proposedContentOffset.x > self.previousOffsetX + self.itemSize.width / 3.0) { self.previousOffsetX += self.collectionView.frame.size.width - self.minimumLineSpacing * 2; } else if (proposedContentOffset.x < self.previousOffsetX - self.itemSize.width / 3.0) { self.previousOffsetX -= self.collectionView.frame.size.width - self.minimumLineSpacing * 2; } proposedContentOffset.x = self.previousOffsetX; } else { CGFloat x = proposedContentOffset.x / (self.itemSize.width + self.minimumLineSpacing); int base = (int)x; proposedContentOffset.x = base * (self.itemSize.width + self.minimumLineSpacing); } return proposedContentOffset; } 这里是以1/3.0为分界,左、右的1/3作为分界线,超过才会切换过去! 感谢 感谢评论的朋友们的一句话,点醒了笔者。对于不分页的情况下,其实只要使用当前的偏移x除(itemSize.width + minimumLineSpacing)就得到一个倍数,然后四舍五入。比如,4.3取整得到4,那么就是没有超过一半,就要往回滚。而4.6取整得到5,表示要滚动到下一个。所以在不分页的情况下,其实也是非常简单的。 结尾 本篇文章经过朋友们的评论及反馈,可以说已经完善了!
概述 UICollectionView真的好强大,今天我们来研究一下这种很常见的卡片动画效果是如何实现了。本篇不能太深入地讲解,因为笔者也是刚刚摸索出点眉目,但是并没有深刻地理解。如果在讲解过程中,出现不对的地方,请及时反馈。 效果图 重写API 1 2 3 4 5 6 7 8 9 10 11 12 // 我们必须重写此方法,指定布局大小 // 每次layout invalidated或者重新query布局信息时,会调用 - (void)prepareLayout; // 用于决定布局信息 // 我们必须重写它来实现布局信息 - (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect; // 重写它来布局信息 - (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath; 还有一个非常关键的API,必须重写: 1 2 3 4 5 // return YES to cause the collection view to requery the layout for geometry information // 当重新查询布局信息时,就会调用此API。要设置为YES,才能实现自定义布局。 - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds; 自定义布局 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 // // HYBCardFlowLayout.m // CollectionViewDemos // // Created by huangyibiao on 16/3/26. // Copyright © 2016年 huangyibiao. All rights reserved. // #import "HYBCardFlowLayout.h" @interface HYBCardFlowLayout () @property (nonatomic, strong) NSIndexPath *mainIndexPath; @property (nonatomic, strong) NSIndexPath *willMoveToMainIndexPath; @end @implementation HYBCardFlowLayout - (void)prepareLayout { CGFloat inset = 32; self.itemSize = CGSizeMake(self.collectionView.frame.size.width - 2 * inset, self.collectionView.frame.size.height * 3 / 4); self.sectionInset = UIEdgeInsetsMake(0, inset, 0, inset); self.scrollDirection = UICollectionViewScrollDirectionHorizontal; [super prepareLayout]; } #pragma mark - Override - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { return YES; } - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { UICollectionViewLayoutAttributes *attribute = [super layoutAttributesForItemAtIndexPath:indexPath]; [self setTransformForLayoutAttributes:attribute]; return attribute; } - (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect { NSArray *attributesSuper = [super layoutAttributesForElementsInRect:rect]; // 一定要深复制一份,不能修改父类的属性,不然会有很多error打印出来 NSArray *attributes = [[NSArray alloc] initWithArray:attributesSuper copyItems:YES]; NSArray *visibleIndexPaths = [self.collectionView indexPathsForVisibleItems]; if (visibleIndexPaths.count <= 0) { return attributes; } else if (visibleIndexPaths.count == 1) { self.mainIndexPath = [visibleIndexPaths firstObject]; self.willMoveToMainIndexPath = nil; } else if (visibleIndexPaths.count == 2) { NSIndexPath *indexPath = [visibleIndexPaths firstObject]; // 说明是往左滚动 if (indexPath == self.mainIndexPath) { // 记录将要移进来的位置 self.willMoveToMainIndexPath = visibleIndexPaths[1]; } else {// 往右滚动 self.willMoveToMainIndexPath = visibleIndexPaths[0]; // 更新下一个成为main self.mainIndexPath = visibleIndexPaths[1]; } } for (UICollectionViewLayoutAttributes *attribute in attributes) { [self setTransformForLayoutAttributes:attribute]; } return attributes; } - (void)setTransformForLayoutAttributes:(UICollectionViewLayoutAttributes *)attribute { UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:attribute.indexPath]; if (self.mainIndexPath && attribute.indexPath.section == self.mainIndexPath.section) { attribute.transform3D = [self tranformForView:cell]; } else if (self.willMoveToMainIndexPath && attribute.indexPath.section == self.willMoveToMainIndexPath.section) { attribute.transform3D = [self tranformForView:cell]; } } - (CATransform3D)tranformForView:(UICollectionViewCell *)view { // cell的起始偏移 CGFloat w = self.collectionView.frame.size.width; CGFloat offset = [self.collectionView indexPathForCell:view].section * w; // 当前偏移 CGFloat currentOffset = self.collectionView.contentOffset.x; // 计算偏移angle CGFloat angle = (currentOffset - offset) / w; CATransform3D t = CATransform3DIdentity; t.m34 = 1.0 / -500; if (currentOffset - offset >= 0) { t = CATransform3DRotate(t, angle, 1, 1, 0); } else { t = CATransform3DRotate(t, angle, -1, 1, 0); } return t; } @end 这里主要是要处理旋转。然后要处理切换cell的attribute设置。mainIndexPath属性用于记录当前显示的cell的位置。willMoveToMainIndexPath记录将要出现的cell的位置。 结尾 这里在慢慢切换时,效果是挺好的,但是如果快速切换卡片,你会发现会有一点点不好之处,就是下一个cell突然出现的。
概述 突然想起小美到家App中的一个列表效果,反正正好最近在研究collectonview,正在拿它个效果来练练手。今天教大家如何实现竖直滚动的列表缩放式效果,体验果然提高了不少。 实现效果 缘由 10个月前的事了,那时候在一个很小的创业公司里做美容O2O的,然后研究了很多同行的App,看着人家的效果却不知如何入手,只怪当初太菜。 利用周末研究了一下,果然还是实现出来了。哈哈,时隔这么久还是不能忘记这份“耻辱”啊。不过上周末可不只是实现了这种效果哦,还有好几种效果的。大家可以看文章末尾的推荐阅读。 实现思路 从效果图可以看到变化是,越是往中间滚动的item显示最大,越显眼。而越是往前面,或者越是后面的,反而显示越小,这样就形成了视觉差。 实现的思路就是通过重写在可见范围内的所有item的方法: 1 2 3 - (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect 通过这个API可以获取到原始属性,然后利用公式来计算缩放。 难点 如何计算缩放系数。这里实现的效果的公式如下: 1 2 3 CGFloat zoom = 1 + 0.15 * (1 - fabs(widthForScale)); 其中,widthForScale是最难把握的系数。 如何计算widthForScale呢?方法是利用item的中心y与当前collectionview的contentOffset.y来计算: 1 2 3 4 CGFloat distance = obj.center.y - fabs(offset) - self.itemSize.width; CGFloat widthForScale = distance / self.itemSize.height; 其中,offset这么计算出来的: 1 2 3 4 5 6 7 CGRect visibleRect = CGRectZero; visibleRect.origin = self.collectionView.contentOffset; visibleRect.size = self.collectionView.frame.size; CGFloat offset = CGRectGetMinY(visibleRect); 最大的难点是distance的计算,如何拿捏呢?这需要慢慢分析。 假设有三个item显示,那么第一个item的缩放要比第二个item(主显示的item)要小,然后第二个item的缩放要比第三个要大。 第二个item(主)比第一个多了一个item,而第三个又比第二个多了一个item,所以再减去一个item的宽就可以了。 核心代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 - (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect { NSArray *superAttributes = [super layoutAttributesForElementsInRect:rect]; NSArray *attributes = [[NSArray alloc] initWithArray:superAttributes copyItems:YES]; CGRect visibleRect = CGRectZero; visibleRect.origin = self.collectionView.contentOffset; visibleRect.size = self.collectionView.frame.size; CGFloat offset = CGRectGetMinY(visibleRect); [attributes enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *obj, NSUInteger idx, BOOL * _Nonnull stop) { CGFloat distance = obj.center.y - fabs(offset) - self.itemSize.width; CGFloat widthForScale = distance / self.itemSize.height; CGFloat zoom = 1 + 0.15 * (1 - fabs(widthForScale)); obj.transform3D = CATransform3DMakeScale(zoom, 1.0, 1.0); }]; return attributes; } 系数0.15自由调整。 结尾 这里没有实现分页的特性,笔者还是尝试了的,不过因为一个item不是占满全屏,所以计算起来总是达不到笔者期望的效果。大家若能在笔者的基础上研究出来,请主动通知笔者,谢谢! 源代码 CollectionViewDemos 提示:本篇文章的demo对应于工程中的Demo4-CardLayout-vertical-scale分组。 推荐阅读 CollectionView网格布局 CollectionView旋转水平卡片布局 CollectionView缩放水平卡片布局
前言 上一篇专门讲解了WKWebView相关的所有类、代理的所有API。那么本篇讲些什么呢?当然是实战了! 本篇文章教大家如何使用WKWebView去实现常用的一些API操作。当然,也会有如何与JS交互的实战。 如果还没有阅读过WKWebView精讲(OC版),请先阅读,不然有可能看不懂下面所讲的内容。 效果图 通过本篇文章,至少可以学习到: OC如何给JS注入对象及JS如何给IOS发送数据 JS调用alert、confirm、prompt时,不采用JS原生提示,而是使用iOS原生来实现 如何监听web内容加载进度、是否加载完成 如何处理去跨域问题 创建配置类 在创建WKWebView之前,需要先创建配置对象,用于做一些配置: 1 2 3 WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init]; 配置偏好设置 偏好设置也没有必须去修改它,都使用默认的就可以了,除非你真的需要修改它: 1 2 3 4 5 6 7 8 9 10 // 设置偏好设置 config.preferences = [[WKPreferences alloc] init]; // 默认为0 config.preferences.minimumFontSize = 10; // 默认认为YES config.preferences.javaScriptEnabled = YES; // 在iOS上默认为NO,表示不能自动通过窗口打开 config.preferences.javaScriptCanOpenWindowsAutomatically = NO; 配置web内容处理池 其实我们没有必要去创建它,因为它根本没有属性和方法: 1 2 3 4 // web内容处理池,由于没有属性可以设置,也没有方法可以调用,不用手动创建 config.processPool = [[WKProcessPool alloc] init]; 配置Js与Web内容交互 WKUserContentController是用于给JS注入对象的,注入对象后,JS端就可以使用: 1 2 3 window.webkit.messageHandlers.<name>.postMessage(<messageBody>) 来调用发送数据给iOS端,比如: 1 2 3 window.webkit.messageHandlers.AppModel.postMessage({body: '传数据'}); AppModel就是我们要注入的名称,注入以后,就可以在JS端调用了,传数据统一通过body传,可以是多种类型,只支持NSNumber, NSString, NSDate, NSArray,NSDictionary, and NSNull类型。 下面我们配置给JS的main frame注入AppModel名称,对于JS端可就是对象了: 1 2 3 4 5 6 7 8 // 通过JS与webview内容交互 config.userContentController = [[WKUserContentController alloc] init]; // 注入JS对象名称AppModel,当JS通过AppModel来调用时, // 我们可以在WKScriptMessageHandler代理中接收到 [config.userContentController addScriptMessageHandler:self name:@"AppModel"]; 当JS通过AppModel发送数据到iOS端时,会在代理中收到: 1 2 3 4 5 6 7 8 9 10 11 #pragma mark - WKScriptMessageHandler - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { if ([message.name isEqualToString:@"AppModel"]) { // 打印所传过来的参数,只支持NSNumber, NSString, NSDate, NSArray, // NSDictionary, and NSNull类型 NSLog(@"%@", message.body); } } 所有JS调用iOS的部分,都只可以在此处使用哦。当然我们也可以注入多个名称(JS对象),用于区分功能。 创建WKWebView 通过唯一的默认构造器来创建对象: 1 2 3 4 5 self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config]; [self.view addSubview:self.webView]; 加载H5页面 1 2 3 4 NSURL *path = [[NSBundle mainBundle] URLForResource:@"test" withExtension:@"html"]; [self.webView loadRequest:[NSURLRequest requestWithURL:path]]; 配置代理 如果需要处理web导航条上的代理处理,比如链接是否可以跳转或者如何跳转,需要设置代理;而如果需要与在JS调用alert、confirm、prompt函数时,通过JS原生来处理,而不是调用JS的alert、confirm、prompt函数,那么需要设置UIDelegate,在得到响应后可以将结果反馈到JS端: 1 2 3 4 5 6 // 导航代理 self.webView.navigationDelegate = self; // 与webview UI交互代理 self.webView.UIDelegate = self; 添加对WKWebView属性的监听 WKWebView有好多个支持KVO的属性,这里只是监听loading、title、estimatedProgress属性,分别用于判断是否正在加载、获取页面标题、当前页面载入进度: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // 添加KVO监听 [self.webView addObserver:self forKeyPath:@"loading" options:NSKeyValueObservingOptionNew context:nil]; [self.webView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:nil]; [self.webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil]; 然后我们就可以实现KVO处理方法,在loading完成时,可以注入一些JS到web中。这里只是简单地执行一段web中的JS函数: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #pragma mark - KVO - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context { if ([keyPath isEqualToString:@"loading"]) { NSLog(@"loading"); } else if ([keyPath isEqualToString:@"title"]) { self.title = self.webView.title; } else if ([keyPath isEqualToString:@"estimatedProgress"]) { NSLog(@"progress: %f", self.webView.estimatedProgress); self.progressView.progress = self.webView.estimatedProgress; } // 加载完成 if (!self.webView.loading) { // 手动调用JS代码 // 每次页面完成都弹出来,大家可以在测试时再打开 NSString *js = @"callJsAlert()"; [self.webView evaluateJavaScript:js completionHandler:^(id _Nullable response, NSError * _Nullable error) { NSLog(@"response: %@ error: %@", response, error); NSLog(@"call js alert by native"); }]; [UIView animateWithDuration:0.5 animations:^{ self.progressView.alpha = 0; }]; } } WKUIDelegate 与JS原生的alert、confirm、prompt交互,将弹出来的实际上是我们原生的窗口,而不是JS的。在得到数据后,由原生传回到JS: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #pragma mark - WKUIDelegate - (void)webViewDidClose:(WKWebView *)webView { NSLog(@"%s", __FUNCTION__); } // 在JS端调用alert函数时,会触发此代理方法。 // JS端调用alert时所传的数据可以通过message拿到 // 在原生得到结果后,需要回调JS,是通过completionHandler回调 - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler { NSLog(@"%s", __FUNCTION__); UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"alert" message:@"JS调用alert" preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { completionHandler(); }]]; [self presentViewController:alert animated:YES completion:NULL]; NSLog(@"%@", message); } // JS端调用confirm函数时,会触发此方法 // 通过message可以拿到JS端所传的数据 // 在iOS端显示原生alert得到YES/NO后 // 通过completionHandler回调给JS端 - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler { NSLog(@"%s", __FUNCTION__); UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"confirm" message:@"JS调用confirm" preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { completionHandler(YES); }]]; [alert addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { completionHandler(NO); }]]; [self presentViewController:alert animated:YES completion:NULL]; NSLog(@"%@", message); } // JS端调用prompt函数时,会触发此方法 // 要求输入一段文本 // 在原生输入得到文本内容后,通过completionHandler回调给JS - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * __nullable result))completionHandler { NSLog(@"%s", __FUNCTION__); NSLog(@"%@", prompt); UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"textinput" message:@"JS调用输入框" preferredStyle:UIAlertControllerStyleAlert]; [alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) { textField.textColor = [UIColor redColor]; }]; [alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { completionHandler([[alert.textFields lastObject] text]); }]]; [self presentViewController:alert animated:YES completion:NULL]; } WKNavigationDelegate 如果需要处理web导航操作,比如链接跳转、接收响应、在导航开始、成功、失败等时要做些处理,就可以通过实现相关的代理方法: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 #pragma mark - WKNavigationDelegate // 请求开始前,会先调用此代理方法 // 与UIWebView的 // - (BOOL)webView:(UIWebView *)webView // shouldStartLoadWithRequest:(NSURLRequest *)request // navigationType:(UIWebViewNavigationType)navigationType; // 类型,在请求先判断能不能跳转(请求) - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { NSString *hostname = navigationAction.request.URL.host.lowercaseString; if (navigationAction.navigationType == WKNavigationTypeLinkActivated && ![hostname containsString:@".baidu.com"]) { // 对于跨域,需要手动跳转 [[UIApplication sharedApplication] openURL:navigationAction.request.URL]; // 不允许web内跳转 decisionHandler(WKNavigationActionPolicyCancel); } else { self.progressView.alpha = 1.0; decisionHandler(WKNavigationActionPolicyAllow); } NSLog(@"%s", __FUNCTION__); } // 在响应完成时,会回调此方法 // 如果设置为不允许响应,web内容就不会传过来 - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler { decisionHandler(WKNavigationResponsePolicyAllow); NSLog(@"%s", __FUNCTION__); } // 开始导航跳转时会回调 - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation { NSLog(@"%s", __FUNCTION__); } // 接收到重定向时会回调 - (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation { NSLog(@"%s", __FUNCTION__); } // 导航失败时会回调 - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error { NSLog(@"%s", __FUNCTION__); } // 页面内容到达main frame时回调 - (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation { NSLog(@"%s", __FUNCTION__); } // 导航完成时,会回调(也就是页面载入完成了) - (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation { NSLog(@"%s", __FUNCTION__); } // 导航失败时会回调 - (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error { } // 对于HTTPS的都会触发此代理,如果不要求验证,传默认就行 // 如果需要证书验证,与使用AFN进行HTTPS证书验证是一样的 - (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *__nullable credential))completionHandler { NSLog(@"%s", __FUNCTION__); completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); } // 9.0才能使用,web内容处理中断时会触发 - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView { NSLog(@"%s", __FUNCTION__); } JS端代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 <!DOCTYPE html> <html> <head> <title>iOS and Js</title> <style type="text/css"> * { font-size: 40px; } </style> </head> <body> <div style="margin-top: 100px"> <h1>Test how to use objective-c call js</h1><br/> <div><input type="button" value="call js alert" onclick="callJsAlert()"></div> <br/> <div><input type="button" value="Call js confirm" onclick="callJsConfirm()"></div><br/> </div> <br/> <div> <div><input type="button" value="Call Js prompt " onclick="callJsInput()"></div><br/> <div>Click me here: <a href="http://www.baidu.com">Jump to Baidu</a></div> </div> <br/> <div id="SwiftDiv"> <span id="jsParamFuncSpan" style="color: red; font-size: 50px;"></span> </div> <script type="text/javascript"> function callJsAlert() { alert('Objective-C call js to show alert'); window.webkit.messageHandlers.AppModel.postMessage({body: 'call js alert in js'}); } function callJsConfirm() { if (confirm('confirm', 'Objective-C call js to show confirm')) { document.getElementById('jsParamFuncSpan').innerHTML = 'true'; } else { document.getElementById('jsParamFuncSpan').innerHTML = 'false'; } // AppModel是我们所注入的对象 window.webkit.messageHandlers.AppModel.postMessage({body: 'call js confirm in js'}); } function callJsInput() { var response = prompt('Hello', 'Please input your name:'); document.getElementById('jsParamFuncSpan').innerHTML = response; // AppModel是我们所注入的对象 window.webkit.messageHandlers.AppModel.postMessage({body: response}); } </script> </body> </html> 源代码 下载源代码:WKWebViewH5ObjCDemo 关注GITHUB:CoderJackyHuang 推荐阅读 WKWebView精讲-OC版 WebViewJavascriptBridge详细使用 OC JavaScriptCore与js交互 Swift WKWebView新特性及JS交互
前言 鉴于LL同志对笔者说:“能不能写个OC版本的WKWebView的使用教程?”,还积极打赏了30RMB,笔者又怎么好意思拒绝呢,于是才有了下文。 所有看到本篇文章的同志们,应该要感谢LL同志,更要向LL同志学习,积极打赏! WKWebView 看看WKWebView的头文件声明: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 // webview 配置,具体看下面 @property (nonatomic, readonly, copy) WKWebViewConfiguration *configuration; // 导航代理 @property (nullable, nonatomic, weak) id <WKNavigationDelegate> navigationDelegate; // 用户交互代理 @property (nullable, nonatomic, weak) id <WKUIDelegate> UIDelegate; // 页面前进、后退列表 @property (nonatomic, readonly, strong) WKBackForwardList *backForwardList; // 默认构造器 - (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER; // 已不再使用 - (instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE; // 与UIWebView一样的加载请求API - (nullable WKNavigation *)loadRequest:(NSURLRequest *)request; // 加载URL - (nullable WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL NS_AVAILABLE(10_11, 9_0); // 直接加载HTML - (nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL; // 直接加载data - (nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString *)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL NS_AVAILABLE(10_11, 9_0); // 前进或者后退到某一页面 - (nullable WKNavigation *)goToBackForwardListItem:(WKBackForwardListItem *)item; // 页面的标题,这昆支持KVO的 @property (nullable, nonatomic, readonly, copy) NSString *title; // 当前请求的URL,它是支持KVO的 @property (nullable, nonatomic, readonly, copy) NSURL *URL; // 标识当前是否正在加载内容中,它是支持KVO的 @property (nonatomic, readonly, getter=isLoading) BOOL loading; // 当前加载的进度,范围为[0, 1] @property (nonatomic, readonly) double estimatedProgress; // 标识页面中的所有资源是否通过安全加密连接来加载,它是支持KVO的 @property (nonatomic, readonly) BOOL hasOnlySecureContent; // 当前导航的证书链,支持KVO @property (nonatomic, readonly, copy) NSArray *certificateChain NS_AVAILABLE(10_11, 9_0); // 是否可以招待goback操作,它是支持KVO的 @property (nonatomic, readonly) BOOL canGoBack; // 是否可以执行gofarward操作,支持KVO @property (nonatomic, readonly) BOOL canGoForward; // 返回上一页面,如果不能返回,则什么也不干 - (nullable WKNavigation *)goBack; // 进入下一页面,如果不能前进,则什么也不干 - (nullable WKNavigation *)goForward; // 重新载入页面 - (nullable WKNavigation *)reload; // 重新从原始URL载入 - (nullable WKNavigation *)reloadFromOrigin; // 停止加载数据 - (void)stopLoading; // 执行JS代码 - (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ __nullable)(__nullable id, NSError * __nullable error))completionHandler; // 标识是否支持左、右swipe手势是否可以前进、后退 @property (nonatomic) BOOL allowsBackForwardNavigationGestures; // 自定义user agent,如果没有则为nil @property (nullable, nonatomic, copy) NSString *customUserAgent NS_AVAILABLE(10_11, 9_0); // 在iOS上默认为NO,标识不允许链接预览 @property (nonatomic) BOOL allowsLinkPreview NS_AVAILABLE(10_11, 9_0); #if TARGET_OS_IPHONE /*! @abstract The scroll view associated with the web view. */ @property (nonatomic, readonly, strong) UIScrollView *scrollView; #endif #if !TARGET_OS_IPHONE // 标识是否支持放大手势,默认为NO @property (nonatomic) BOOL allowsMagnification; // 放大因子,默认为1 @property (nonatomic) CGFloat magnification; // 根据设置的缩放因子来缩放页面,并居中显示结果在指定的点 - (void)setMagnification:(CGFloat)magnification centeredAtPoint:(CGPoint)point; #endif WKWebViewConfiguration配置 1 2 3 WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init]; WKPreferences偏好设置 目前在iOS平台上偏好设置只有三个属性可以设置,如下: 1 2 3 4 5 6 7 8 9 10 // 设置偏好设置 config.preferences = [[WKPreferences alloc] init]; // 默认为0 config.preferences.minimumFontSize = 10; // 默认认为YES config.preferences.javaScriptEnabled = YES; // 在iOS上默认为NO,表示不能自动通过窗口打开 config.preferences.javaScriptCanOpenWindowsAutomatically = NO; WKProcessPool内容处理池 WKProcessPool并没有公开任何的属性或者方法,不需要配置: 1 2 3 config.processPool = [[WKProcessPool alloc] init]; 其实我们也没有必须去创建它。 WKUserContentController内容交互控制器 我们要通过JS与webview内容交互,就需要到这个类了,它的所有属性及方法说明如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 只读属性,所有添加的WKUserScript都在这里可以获取到 @property (nonatomic, readonly, copy) NSArray<WKUserScript *> *userScripts; // 注入JS - (void)addUserScript:(WKUserScript *)userScript; // 移除所有注入的JS - (void)removeAllUserScripts; // 添加scriptMessageHandler到所有的frames中,则都可以通过 // window.webkit.messageHandlers.<name>.postMessage(<messageBody>) // 发送消息 // 比如,JS要调用我们原生的方法,就可以通过这种方式了 - (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name; // 根据name移除所注入的scriptMessageHandler - (void)removeScriptMessageHandlerForName:(NSString *)name; WKUserScript 在WKUserContentController中,所有使用到WKUserScript。WKUserContentController是用于与JS交互的类,而所注入的JS是WKUserScript对象。它的所有属性和方法如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // JS源代码 @property (nonatomic, readonly, copy) NSString *source; // JS注入时间 @property (nonatomic, readonly) WKUserScriptInjectionTime injectionTime; // 只读属性,表示JS是否应该注入到所有的frames中还是只有main frame. @property (nonatomic, readonly, getter=isForMainFrameOnly) BOOL forMainFrameOnly; // 初始化方法,用于创建WKUserScript对象 // source:JS源代码 // injectionTime:JS注入的时间 // forMainFrameOnly:是否只注入main frame - (instancetype)initWithSource:(NSString *)source injectionTime:(WKUserScriptInjectionTime)injectionTime forMainFrameOnly:(BOOL)forMainFrameOnly; WKUserScriptInjectionTime 1 2 3 4 5 6 typedef NS_ENUM(NSInteger, WKUserScriptInjectionTime) { WKUserScriptInjectionTimeAtDocumentStart, WKUserScriptInjectionTimeAtDocumentEnd } NS_ENUM_AVAILABLE(10_10, 8_0); 它是一个枚举类型,只有在文档开始加载时注入和加载结束时注入。 WKWebsiteDataStore存储的Web内容 iOS9.0以后才能使用这个类。它是代表webview不同的数据类型,包括cookies、disk、memory caches、WebSQL、IndexedDB数据库和本地存储。 从这里看,要优化Webview好像可以通过它来实现,不过要求iOS9.0以上才能使用。现在6.0都没有抛弃的我,从来不能考虑这种最新的。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // 默认数据存储 + (WKWebsiteDataStore *)defaultDataStore; // 返回非持久化存储,数据不会写入文件系统 + (WKWebsiteDataStore *)nonPersistentDataStore; // 已经不可用 - (instancetype)init NS_UNAVAILABLE; // 只读属性,表示是否是持久化存储 @property (nonatomic, readonly, getter=isPersistent) BOOL persistent; // 获取所有web内容的数据存储类型集,比如cookies、disk等 + (NSSet<NSString *> *)allWebsiteDataTypes; // 获取某些指定数据存储类型的数据 - (void)fetchDataRecordsOfTypes:(NSSet<NSString *> *)dataTypes completionHandler:(void (^)(NSArray<WKWebsiteDataRecord *> *))completionHandler; // 删除某些指定类型的数据 - (void)removeDataOfTypes:(NSSet<NSString *> *)dataTypes forDataRecords:(NSArray<WKWebsiteDataRecord *> *)dataRecords completionHandler:(void (^)(void))completionHandler; // 删除某些指定类型的数据且修改日期是指定的日期 - (void)removeDataOfTypes:(NSSet<NSString *> *)websiteDataTypes modifiedSince:(NSDate *)date completionHandler:(void (^)(void))completionHandler; 所有的dataTypes是下面这些系统所定义的: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 WK_EXTERN NSString * const WKWebsiteDataTypeDiskCache NS_AVAILABLE(10_11, 9_0); WK_EXTERN NSString * const WKWebsiteDataTypeMemoryCache NS_AVAILABLE(10_11, 9_0); WK_EXTERN NSString * const WKWebsiteDataTypeOfflineWebApplicationCache NS_AVAILABLE(10_11, 9_0); WK_EXTERN NSString * const WKWebsiteDataTypeCookies NS_AVAILABLE(10_11, 9_0); WK_EXTERN NSString * const WKWebsiteDataTypeSessionStorage NS_AVAILABLE(10_11, 9_0); WK_EXTERN NSString * const WKWebsiteDataTypeLocalStorage NS_AVAILABLE(10_11, 9_0); WK_EXTERN NSString * const WKWebsiteDataTypeWebSQLDatabases NS_AVAILABLE(10_11, 9_0); WK_EXTERN NSString * const WKWebsiteDataTypeIndexedDBDatabases NS_AVAILABLE(10_11, 9_0); WKWebsiteDataRecord iOS9.0以后才可用。 website的数据存储记录类型,它只有两个属性: 1 2 3 4 5 6 7 // 通常是域名 @property (nonatomic, readonly, copy) NSString *displayName; // 存储的数据类型集 @property (nonatomic, readonly, copy) NSSet<NSString *> *dataTypes; WKSelectionGranularity选择粒度 它表示在webview上选择内容的粒度,只有下面这两种类型: 1 2 3 4 5 6 typedef NS_ENUM(NSInteger, WKSelectionGranularity) { WKSelectionGranularityDynamic, WKSelectionGranularityCharacter, } NS_ENUM_AVAILABLE_IOS(8_0); 它是用于webview内容交互时选择内容的粒度类型设置。比如说,当使用WKSelectionGranularityDynamic时,而所选择的内容是单个块,这时候granularity可能会是单个字符;当所选择的web内容不限制于某个块时,granularity可能会是单个块。 WKNavigationDelegate 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @protocol WKNavigationDelegate <NSObject> @optional // 决定导航的动作,通常用于处理跨域的链接能否导航。WebKit对跨域进行了安全检查限制,不允许跨域,因此我们要对不能跨域的链接 // 单独处理。但是,对于Safari是允许跨域的,不用这么处理。 // 这个是决定是否Request - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler; // 决定是否接收响应 // 这个是决定是否接收response // 要获取response,通过WKNavigationResponse对象获取 - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler; // 当main frame的导航开始请求时,会调用此方法 - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation; // 当main frame接收到服务重定向时,会回调此方法 - (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation; // 当main frame开始加载数据失败时,会回调 - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error; // 当main frame的web内容开始到达时,会回调 - (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation; // 当main frame导航完成时,会回调 - (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation; // 当main frame最后下载数据失败时,会回调 - (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error; // 这与用于授权验证的API,与AFN、UIWebView的授权验证API是一样的 - (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *__nullable credential))completionHandler; // 当web content处理完成时,会回调 - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView NS_AVAILABLE(10_11, 9_0); @end WKNavigationActionPolicy 导航动作决定策略: 1 2 3 4 5 6 typedef NS_ENUM(NSInteger, WKNavigationActionPolicy) { WKNavigationActionPolicyCancel, WKNavigationActionPolicyAllow, } NS_ENUM_AVAILABLE(10_10, 8_0); 它是枚举类型,只有Cancel和Allow这两种。设置为Cancel就是不允许导航,就不会跳转链接。 WKNavigationResponsePolicy 1 2 3 4 5 6 typedef NS_ENUM(NSInteger, WKNavigationResponsePolicy) { WKNavigationResponsePolicyCancel, WKNavigationResponsePolicyAllow, } NS_ENUM_AVAILABLE(10_10, 8_0); WKNavigationResponse WKNavigationResponse是导航响应类,通过它可以获取相关响应的信息: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 NS_CLASS_AVAILABLE(10_10, 8_0) @interface WKNavigationResponse : NSObject // 是否是main frame @property (nonatomic, readonly, getter=isForMainFrame) BOOL forMainFrame; // 获取响应response @property (nonatomic, readonly, copy) NSURLResponse *response; // 是否显示MIMEType @property (nonatomic, readonly) BOOL canShowMIMEType; @end 只有接收响应与不接收响应两种。 WKNavigationAction WKNavigationAction对象包含关于导航的action的信息,用于make policy decisions。它只有以下几个属性: 1 2 3 4 5 6 7 8 9 10 11 12 13 // 正在请求的导航的frame @property (nonatomic, readonly, copy) WKFrameInfo *sourceFrame; // 目标frame,如果这是新的window,它会是nil @property (nullable, nonatomic, readonly, copy) WKFrameInfo *targetFrame; // 导航类型,如下面的小标题WKNavigationType @property (nonatomic, readonly) WKNavigationType navigationType; // 导航的请求 @property (nonatomic, readonly, copy) NSURLRequest *request; WKNavigationType WKNavigationType类型是枚举类型,它的可选值如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 typedef NS_ENUM(NSInteger, WKNavigationType) { // 链接已经点击 WKNavigationTypeLinkActivated, // 表单提交 WKNavigationTypeFormSubmitted, // 前进、后退 WKNavigationTypeBackForward, // 重新载入 WKNavigationTypeReload, // 表单重新提交 WKNavigationTypeFormResubmitted, // 其它 WKNavigationTypeOther = -1, } NS_ENUM_AVAILABLE(10_10, 8_0); WKUIDelegate 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @protocol WKUIDelegate <NSObject> @optional // 创建新的webview // 可以指定配置对象、导航动作对象、window特性 - (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures; // webview关闭时回调 - (void)webViewDidClose:(WKWebView *)webView NS_AVAILABLE(10_11, 9_0); // 调用JS的alert()方法 - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler; // 调用JS的confirm()方法 - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler; // 调用JS的prompt()方法 - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * __nullable result))completionHandler; @end WKBackForwardList WKBackForwardList表示webview中可以前进或者后退的页面列表。其声明如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 NS_CLASS_AVAILABLE(10_10, 8_0) @interface WKBackForwardList : NSObject // 当前正在显示的item(页面) @property (nullable, nonatomic, readonly, strong) WKBackForwardListItem *currentItem; // 后一页,如果没有就是nil @property (nullable, nonatomic, readonly, strong) WKBackForwardListItem *backItem; // 前一页,如果没有就是nil @property (nullable, nonatomic, readonly, strong) WKBackForwardListItem *forwardItem; // 根据下标获取某一个页面的item - (nullable WKBackForwardListItem *)itemAtIndex:(NSInteger)index; // 可以进行goback操作的页面列表 @property (nonatomic, readonly, copy) NSArray<WKBackForwardListItem *> *backList; // 可以进行goforward操作的页面列表 @property (nonatomic, readonly, copy) NSArray<WKBackForwardListItem *> *forwardList; @end WKBackForwardListItem 页面导航前进、后退列表项: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 NS_CLASS_AVAILABLE(10_10, 8_0) @interface WKBackForwardListItem : NSObject // 该页面的URL @property (readonly, copy) NSURL *URL; // 该页面的title @property (nullable, readonly, copy) NSString *title; // 初始请求该item的请求的URL @property (readonly, copy) NSURL *initialURL; @end 最后 本篇文章只是讲解了WKWebView所有相关的类的API,先阅读过本篇文章,再继续往下阅读实战篇文章! 关注下一篇:WKWebView与H5交互实战