实现思路
将文本和图片拼接为HTML代码。
使用JavaScript添加点击事件。
使用MagicWebViewWebP提供UIWebView加载webp格式图片支持。
使用UIWebView加载HTML代码。
使用UIWebView代理方法,拦截页面发出的请求,获取selectIndex。
实现效果
组件 | 描述 | 说明 |
---|---|---|
UIScrollView | 根容器 | 高度自适应(KVO处理UIWebView + UICollectionView高度) |
UIWebView | 图文混排展示 | 加载HTML代码 |
UICollectionView | 更多推荐展示 | 无 |
实现效果
问题汇总
1、如何实现JavaScript与Objective-C间传值?
点击Webview中的图片,放大,需要JavaScript和Objective-C传值,获取到具体需要放大哪张图片。
本方案中,不需要引入WebViewJavascriptBridge,而是通过【控制Webview重定向方法,拦截发出的请求】来实现。
示例:
// 每个添加点击事件(window.location.href),其中selectIndex为图片标识// webview发起请求拦截 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{ // 获取img标识index NSString *url = request.URL.absoluteString; NSRange range = [url rangeOfString:@"selectIndex="]; if (range.location != NSNotFound) { NSInteger begin = range.location + range.length; NSString *index = [url substringFromIndex:begin]; NSLog(@"img: %@", index); return NO; } return YES; }
2、如何实现UIWebView高度自适应?
UIWebView自适应高度的方案有很多,选择一个较为科学的方式,显得尤为重要。
本方案中,通过【KVO监听Webview的contentSize】来实现,需要注意KVO的添加、移除,稍有不慎有Crash风险。
示例:
// 添加监听 [self.webView.scrollView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil]; - (UIWebView *)webView { if (!_webView) { _webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)]; _webView.delegate = self; _webView.scrollView.bounces = NO; _webView.scrollView.showsHorizontalScrollIndicator = NO; _webView.scrollView.scrollEnabled = NO; _webView.scalesPageToFit = YES; } return _webView; } // 修改webview的frame - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@"contentSize"]) { CGSize resize = [self.webView sizeThatFits:CGSizeZero]; self.webView.frame = CGRectMake(0, 0, CGRectGetWidth(self.frame), resize.height); } } // 移除监听 -(void)dealloc { [self.webView.scrollView removeObserver:self forKeyPath:@"contentSize"]; }
3、如何实现UIWebView显示webp格式图片?
UIWebView、WKWebview本身都不支持webp格式图片,需要额外扩展。
可以直接访问我的GitHub,下载MagicWebViewWebP,将【MagicWebViewWebP.framework】直接导入工程。
参考: UIWebView、WKWebView支持WebP图片显示
示例:
// 导入头文件 #import // 注册 MagicURLProtocol [[MagicWebViewWebPManager shareManager] registerMagicURLProtocolWebView:self.webView]; // 销毁 MagicURLProtocol -(void)dealloc { [[MagicWebViewWebPManager shareManager] unregisterMagicURLProtocolWebView:self.webView]; }
4、如何实现图文混排 + UIKit组件?
使用UIWebView加载自定义HTML代码的方式,实现图文混排。
点击图片,放大,function()跳转链接,携带selectIndex标识,通过拦截UIWebView的请求来获取selectIndex标识。
通过KVO获取到WebView高度,重新设置webView.frame,collectionView.frame,scrollView.contentSize
本方案中,图文混排+UIKit组件,具体逻辑如下:
5、如何自定义HTML代码?
本方案中,以纯图片为例,处理后的HTML如下:
......
6、如何实现并发执行多个网络请求,统一处理?
本方案中,利用GCD创建队列组,提交多个任务到队列组,多个任务同时执行,监听队列组执行完毕,在主线程刷新UI。
注意: dispatch_group_enter() 、 dispatch_group_leave()将队列组中的任务未执行完毕的任务数目加减1(两个函数要配合使用)
参考: 玩转GCD
示例:
- (void)exampleMoreNetwork{ dispatch_group_t group = dispatch_group_create(); dispatch_queue_t serialQueue = dispatch_queue_create("magic_gcd_group", DISPATCH_QUEUE_SERIAL); // 网络请求1 dispatch_group_enter(group); dispatch_group_async(group, serialQueue, ^{ [[MagicNetworkManager shareManager] GET:@"网络请求1" Parameters:nil Success:^(NSURLResponse *response, id responseObject) { dispatch_group_leave(group); } Failure:^(NSURLResponse *response, id error) { dispatch_group_leave(group); }]; }); // 网络请求2 dispatch_group_enter(group); dispatch_group_async(group, serialQueue, ^{ [[MagicNetworkManager shareManager] GET:@"网络请求2" Parameters:nil Success:^(NSURLResponse *response, id responseObject) { dispatch_group_leave(group); } Failure:^(NSURLResponse *response, id error) { dispatch_group_leave(group); }]; }); // 所有网络请求结束 dispatch_group_notify(group, serialQueue, ^{ dispatch_async(dispatch_get_global_queue(0, 0), ^{ dispatch_async(dispatch_get_main_queue(), ^{ // 主线程刷新UI }); }); }); }
图文混排——核心
目录结构
实现代理方法,放大图片,跳转商品,置顶。
实现针对showjoy.com域名,图片url拼接.webp。
实现UIScrollView作为根容器,自适应内容高度。
实现UIWebView支持webp格式图片。
实现自定义HTML代码,图片居中,window.location.href事件传递selectIndex,UIWebView代理拦截selectIndex。
通过HTML,JavaScript,还可以实现更多功能。。。。。。
ProductLoadMorePicTextView.h
#import #import "ProductDetailModel.h" #import "ProductLoadMorePicTextModel.h" @protocol ProductLoadMorePicTextViewDelegate - (void)productLoadMorePicTextViewZoomImageWithIndex:(NSInteger)index; - (void)productLoadMorePicTextViewPushProductWithSkuId:(NSString *)skuId; - (void)productLoadMorePicTextViewGoTop; @end @interface ProductLoadMorePicTextView : UIView @property (nonatomic, weak) id delegate; - (instancetype)initWithFrame:(CGRect)frame productDetailModel:(ProductDetailModel *)productDetailModel picTextModel:(ProductLoadMorePicTextModel *)picTextModel; - (void)reload; @end
ProductLoadMorePicTextView.m
#import "ProductLoadMorePicTextView.h" #import "ProductLoadMorePicTextCollectionViewCell.h" #import "MagicScrollPageRefreshHeader.h" #import static const CGFloat recommendViewHeight = 170.0; static const CGFloat recommendViewSpace = 10.0; static const CGFloat recommendItemWidth = 105.0; static const CGFloat recommendItemSpace = 5.0; static const CGFloat recommendTitleHeight = 40.0; @interface ProductLoadMorePicTextView ()@property (nonatomic, strong) UIScrollView *scrollView; @property (nonatomic, strong) UIWebView *webView; @property (nonatomic, strong) UICollectionView *collectionView; @property (nonatomic, strong) UILabel *recommendLabel; @property (nonatomic, strong) NSMutableArray *recommendDataArray; @property (nonatomic, strong) NSMutableArray *picTextDataArray; @end @implementation ProductLoadMorePicTextView - (instancetype)initWithFrame:(CGRect)frame productDetailModel:(ProductDetailModel *)productDetailModel picTextModel:(ProductLoadMorePicTextModel *)picTextModel { self = [super initWithFrame:frame]; if (self) { self.recommendDataArray = [NSMutableArray arrayWithArray:productDetailModel.recommend]; self.picTextDataArray = [NSMutableArray arrayWithArray:picTextModel.itemPic.packageImages]; [self createSubViewsWithPicTextModel:picTextModel]; } return self; } - (void)createSubViewsWithPicTextModel:(ProductLoadMorePicTextModel *)picTextModel { [self addSubview:self.scrollView]; [[MagicWebViewWebPManager shareManager] registerMagicURLProtocolWebView:self.webView]; [self.scrollView addSubview:self.webView]; [self.scrollView addSubview:self.recommendLabel]; [self.scrollView addSubview:self.collectionView]; [self.webView.scrollView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil]; MC_SELF_WEAK(self) MagicScrollPageRefreshHeader *header = [MagicScrollPageRefreshHeader headerWithRefreshingBlock:^{ [weakself.scrollView.mj_header endRefreshing]; [weakself executeProductLoadMorePicTextViewGoTop]; }]; self.scrollView.mj_header = header; } #pragma mark -Lazy - (UIScrollView *)scrollView { if (!_scrollView) { _scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)]; _scrollView.backgroundColor = [UIColor colorWithRed:0.95 green:0.95 blue:0.95 alpha:1.00]; } return _scrollView; } - (UIWebView *)webView { if (!_webView) { _webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)]; _webView.delegate = self; _webView.scrollView.bounces = NO; _webView.scrollView.showsHorizontalScrollIndicator = NO; _webView.scrollView.scrollEnabled = NO; _webView.scalesPageToFit = YES; } return _webView; } - (UILabel *)recommendLabel{ if (!_recommendLabel) { _recommendLabel = [[UILabel alloc] init]; _recommendLabel.text = @" 更多推荐"; _recommendLabel.textColor = [UIColor colorWithRed:0.30 green:0.30 blue:0.30 alpha:1.00]; _recommendLabel.font = [UIFont systemFontOfSize:12]; _recommendLabel.backgroundColor = [UIColor whiteColor]; } return _recommendLabel; } - (UICollectionView *)collectionView { if (!_collectionView) { UICollectionViewFlowLayout *flowLayout = [UICollectionViewFlowLayout new]; flowLayout.sectionInset = UIEdgeInsetsMake(0, 0, 0, recommendItemSpace); flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal; flowLayout.itemSize = CGSizeMake(recommendItemWidth, recommendViewHeight); _collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:flowLayout]; _collectionView.backgroundColor = [UIColor whiteColor]; _collectionView.delegate = self; _collectionView.dataSource = self; [_collectionView registerClass:[ProductLoadMorePicTextCollectionViewCell class] forCellWithReuseIdentifier:@"cell"]; } return _collectionView; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@"contentSize"]) { CGSize resize = [self.webView sizeThatFits:CGSizeZero]; self.webView.frame = CGRectMake(0, 0, CGRectGetWidth(self.frame), resize.height); self.recommendLabel.frame = CGRectMake(0, CGRectGetMaxY(self.webView.frame) + recommendViewSpace, CGRectGetWidth(self.frame), recommendTitleHeight); self.collectionView.frame = CGRectMake(0, CGRectGetMaxY(self.recommendLabel.frame), CGRectGetWidth(self.frame), recommendViewHeight); self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.frame), CGRectGetMaxY(self.collectionView.frame) + recommendViewSpace); } } -(void)dealloc { [[MagicWebViewWebPManager shareManager] unregisterMagicURLProtocolWebView:self.webView]; [self.webView.scrollView removeObserver:self forKeyPath:@"contentSize"]; self.scrollView = nil; self.webView = nil; self.collectionView = nil; self.recommendDataArray = nil; self.picTextDataArray = nil; } #pragma mark - UIWebViewDelegate - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{ return [self handleWebviewEventWithRequest:request]; } - (void)webViewDidStartLoad:(UIWebView *)webView { } - (void)webViewDidFinishLoad:(UIWebView *)webView { } - (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error { NSLog(@"商品详情web错误 %@", error); } - (BOOL)handleWebviewEventWithRequest:(NSURLRequest *)request { NSString *url = request.URL.absoluteString; NSRange range = [url rangeOfString:@"selectIndex="]; if (range.location != NSNotFound) { NSInteger begin = range.location + range.length; NSString *index = [url substringFromIndex:begin]; [self executeProductLoadMorePicTextViewZoomImageWithIndexString:index]; return NO; } return YES; } #pragma mark - CustomHTML - (void)loadWebViewCustomHTMLWithImageUrls:(NSArray *)imageUrls { NSMutableString *html = [NSMutableString string]; [html appendString:@""]; [html appendString:@""]; [html appendString:@""]; [html appendString:@""]; [html appendString:[self settingWebViewBodyWithImageUrlArray:imageUrls]]; [html appendString:@""]; [html appendString:@""]; [self.webView loadHTMLString:html baseURL:nil]; } - (NSString *)settingWebViewBodyWithImageUrlArray:(NSArray *)imageUrlArray { NSMutableString *body = [NSMutableString string]; for (NSInteger i = 0; i < imageUrlArray.count; i++) { NSString *imgUrl = [NSString stringWithFormat:@"%@", [imageUrlArray objectAtIndex:i]]; imgUrl = [self handlerImgUrlString:imgUrl]; NSMutableString *html = [NSMutableString string]; [html appendString:@""]; NSString *onload = [NSString stringWithFormat:@"this.onclick = function() {window.location.href = 'selectIndex=' + %ld;}", i]; [html appendFormat:@"", onload, imgUrl]; [html appendString:@""]; [body appendString:html]; } return body; } #pragma mark -UICollectionViewDataSource - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{ return self.recommendDataArray.count; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{ ProductLoadMorePicTextCollectionViewCell *cell = (ProductLoadMorePicTextCollectionViewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:@"cell" forIndexPath:indexPath]; cell.productRecommendModel = [self.recommendDataArray objectAtIndex:indexPath.row]; return cell; } #pragma mark -UICollectionViewDelegate - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{ ProductRecommend *productRecommendModel = [self.recommendDataArray objectAtIndex:indexPath.row]; [self executeProductLoadMorePicTextViewPushProductWithSkuId:productRecommendModel.ID]; } #pragma mark -ProductLoadMoreViewDelegate - (void)executeProductLoadMorePicTextViewZoomImageWithIndexString:(NSString *)indexString { if ([self.delegate respondsToSelector:@selector(productLoadMorePicTextViewZoomImageWithIndex:)]) { [self.delegate productLoadMorePicTextViewZoomImageWithIndex:[indexString integerValue]]; } } - (void)executeProductLoadMorePicTextViewPushProductWithSkuId:(NSInteger)skuId { if ([self.delegate respondsToSelector:@selector(productLoadMorePicTextViewPushProductWithSkuId:)]) { [self.delegate productLoadMorePicTextViewPushProductWithSkuId:[NSString stringWithFormat:@"%ld", skuId]]; } } - (void)executeProductLoadMorePicTextViewGoTop { if ([self.delegate respondsToSelector:@selector(productLoadMorePicTextViewGoTop)]) { [self.delegate productLoadMorePicTextViewGoTop]; } } #pragma mark - Reload - (void)reload{ [self loadWebViewCustomHTMLWithImageUrls:self.picTextDataArray]; [self.collectionView reloadData]; } #pragma mark - IMGURL - (NSString *)handlerImgUrlString:(NSString *)imgUrlString { NSString *result = [NetworkManager httpsSchemeHandler:imgUrlString]; // webp if ([result containsString:@"showjoy.com"] && ![result hasSuffix:@".webp"]) { result = [result stringByAppendingString:@".webp"]; } return result; } @end
图文混排——使用
ProductLoadMoreViewController中,保证两个接口都请求完成后,刷新ProductLoadMorePicTextView。
#import "ProductLoadMoreViewController.h" #import "MagicNetworkManager.h" #import "ProductLoadMorePicTextView.h" static NSString * const SJProductAPI = @"https://shopappserver.showjoy.com/api/shop/sku"; static NSString * const SJProductPicTextAPI = @"https://shopappserver.showjoy.com/api/shop/item/pictext"; static NSString * const SJProductSkuId = @"146931"; @interface ProductLoadMoreViewController () @end @implementation ProductLoadMoreViewController{ ProductDetailModel *_productModel; ProductLoadMorePicTextModel *_productPicTextModel; ProductLoadMorePicTextView *_picTextView; } - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. self.view.backgroundColor = [UIColor grayColor]; [self networkRequestData]; } #pragma mark - Network - (void)networkRequestData { [QuicklyHUD showWindowsProgressHUDText:@"加载中..."]; dispatch_group_t group = dispatch_group_create(); dispatch_queue_t serialQueue = dispatch_queue_create("product_group", DISPATCH_QUEUE_SERIAL); // 商品信息 dispatch_group_enter(group); dispatch_group_async(group, serialQueue, ^{ [[MagicNetworkManager shareManager] GET:SJProductAPI Parameters:@{@"skuId" : SJProductSkuId} Success:^(NSURLResponse *response, id responseObject) { [ProductDetailModel mj_setupObjectClassInArray:^NSDictionary *{ return @{@"shop" : [ProductShop class], @"skuList" : [ProductSkuList class], @"value" : [ProductValue class], @"saleInfo" : [ProductSaleInfo class], @"recommend" : [ProductRecommend class], @"skuCommission" : [ProductSkuCommission class], @"item" : [ProductItem class], @"tagSkus" : [ProductTagSkus class], @"tagMap" : [ProductTagMap class], @"skuEnsures" : [ProductSkuEnsures class], @"salesPromotion" : [ProductSalesPromotion class]}; }]; _productModel = [ProductDetailModel mj_objectWithKeyValues:[responseObject valueForKey:@"data"]]; dispatch_group_leave(group); } Failure:^(NSURLResponse *response, id error) { dispatch_group_leave(group); }]; }); // 图文信息 dispatch_group_enter(group); dispatch_group_async(group, serialQueue, ^{ [[MagicNetworkManager shareManager] GET:SJProductPicTextAPI Parameters:@{@"skuId" : SJProductSkuId} Success:^(NSURLResponse *response, id responseObject) { [ProductLoadMorePicTextModel mj_setupObjectClassInArray:^NSDictionary *{ return @{@"item" : [PicTextItem class], @"itemPic" : [PicTextItemPic class], @"spu" : [PicTextSpu class]}; }]; _productPicTextModel = [ProductLoadMorePicTextModel mj_objectWithKeyValues:[responseObject valueForKey:@"data"]]; dispatch_group_leave(group); } Failure:^(NSURLResponse *response, id error) { dispatch_group_leave(group); }]; }); // 主线程刷新UI dispatch_group_notify(group, serialQueue, ^{ dispatch_async(dispatch_get_global_queue(0, 0), ^{ dispatch_async(dispatch_get_main_queue(), ^{ [QuicklyHUD hiddenMBProgressHUDForView:MC_APP_WINDOW]; [self reloadPicTextView]; }); }); }); } #pragma mark - Reload - (void)reloadPicTextView { if (_picTextView) { [_picTextView removeFromSuperview]; _picTextView.delegate = nil; _picTextView = nil; } CGFloat border = 20.0f; _picTextView = [[ProductLoadMorePicTextView alloc] initWithFrame:CGRectMake(border, border, MC_SCREEN_W - 2 * border, MC_SCREEN_H - MC_NAVIGATION_BAR_H - MC_STATUS_BAR_H - 2 * border) productDetailModel:_productModel picTextModel:_productPicTextModel]; _picTextView.delegate = self; [self.view addSubview:_picTextView]; [_picTextView reload]; } #pragma mark - ProductLoadMorePicTextViewDelegate - (void)productLoadMorePicTextViewGoTop { [QuicklyHUD showWindowsOnlyTextHUDText:@"Go Top"]; } - (void)productLoadMorePicTextViewZoomImageWithIndex:(NSInteger)index { [QuicklyHUD showWindowsOnlyTextHUDText:[NSString stringWithFormat:@"img: %ld", index]]; } - (void)productLoadMorePicTextViewPushProductWithSkuId:(NSString *)skuId { [QuicklyHUD showWindowsOnlyTextHUDText:[NSString stringWithFormat:@"skuId: %@", skuId]]; } @end
Demo
MagicCubeKit - 实验室 - ProductLoadMoreViewController
作者:LuisX
链接:https://www.jianshu.com/p/7c0c5b9158e8
LuisX issue https://github.com/Luis-X/Blog/issues