实现思路
- 将文本和图片拼接为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