一.項(xiàng)目需求

二.實(shí)現(xiàn)列表
本次列表展示參考博客為ios - 用UICollectionView實(shí)現(xiàn)瀑布流詳解
具體分為Cell、Layout和Controller三個(gè)層面的實(shí)現(xiàn),實(shí)現(xiàn)邏輯如下:

1.Cell
在Cell層,我們需要對(duì)其進(jìn)行布局(用代碼實(shí)現(xiàn)),類似于Android里面設(shè)置weight一樣,只不過我通過手動(dòng)設(shè)置比例來設(shè)置它們布局的相對(duì)大小。
- (instancetype) initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame: frame]) {
//設(shè)置imageView的布局
_imageView = [[UIImageView alloc] init];
[self.contentView addSubview:_imageView];
[_imageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.contentView).offset(0);
make.size.mas_equalTo(CGSizeMake(self.contentView.bounds.size.width, self.contentView.bounds.size.height * 0.85));
}];
//標(biāo)記為需要重新布局
[_imageView setNeedsLayout];
//設(shè)置label的布局
_titleLabel = [[UILabel alloc] init];
[self.contentView addSubview:_titleLabel];
_titleLabel.textAlignment = NSTextAlignmentCenter;
_titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
_titleLabel.font = [UIFont systemFontOfSize: 13];
[_titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.imageView.mas_bottom).offset(self.contentView.bounds.size.height * 0.04);
make.bottom.equalTo(self.contentView.mas_bottom);
make.size.mas_equalTo(CGSizeMake(self.contentView.bounds.size.width, self.contentView.bounds.size.height * 0.11));
}];
}
return self;
}
有關(guān)setNeedsLayout的可以參考setNeedsLayout與layoutIfNeeded的區(qū)別
為了設(shè)置圓角,我們需要在layoutSublayersOfLayer中設(shè)置圓角,再用setNeedsLayout對(duì)UIImageView進(jìn)行布局更新
- (void) layoutSublayersOfLayer:(CALayer *)layer
{
//設(shè)置圓角
UIBezierPath *maskPath;
maskPath = [UIBezierPath bezierPathWithRoundedRect:self.bounds
byRoundingCorners:(UIRectCornerTopLeft | UIRectCornerTopRight)
cornerRadii:CGSizeMake(5.0f, 5.0f)];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = self.bounds;
maskLayer.path = maskPath.CGPath;
self.layer.mask = maskLayer;
}
2.Layout
在Layout層,我們則需要對(duì)Cell的放置、高度等屬性進(jìn)行相應(yīng)的設(shè)置,由于原demo是可以實(shí)現(xiàn)瀑布流的,其思路是:每次都將cell插入到瀑布流中最短的那一列,然后實(shí)時(shí)更新每一列的高度,直到cell放置結(jié)束。
但是我實(shí)現(xiàn)瀑布流之后發(fā)現(xiàn)效果慘不忍睹(因?yàn)榻o我的封面的高度和寬度參差不齊,不好看),因此套用了demo的模板對(duì)布局進(jìn)行展示:
首先,通過Controller實(shí)現(xiàn)的委托,獲得Cell對(duì)應(yīng)的屬性,將其放入屬性數(shù)組當(dāng)中:
//返回indexPath對(duì)應(yīng)cell的布局屬性
- (UICollectionViewLayoutAttributes *) layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
//創(chuàng)建布局屬性
UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath: indexPath];
//collectionView的寬度
CGFloat collectionViewWidth = self.collectionView.frame.size.width;
//設(shè)置布局屬性的frame
CGFloat cellWidth = (collectionViewWidth - self.edgeInsets.left - self.edgeInsets.right - (self.columnCount - 1) * self.columnMargin) / self.columnCount;
CGFloat cellHeight = cellWidth * 0.8;
//找出最短那一列
NSInteger destColumn = 0;
CGFloat minColumnHeight = [self.columnHeights[0] doubleValue];
for (int i = 1; i < self.columnCount; i++) {
//取得第i列的高度
CGFloat columnHeight = [self.columnHeights[i] doubleValue];
if (minColumnHeight > columnHeight) {
minColumnHeight = columnHeight;
destColumn = I;
}
}
//設(shè)置cell的坐標(biāo)
CGFloat cellX = self.edgeInsets.left + destColumn * (cellWidth + self.columnMargin);
CGFloat cellY = minColumnHeight;
if (cellY != self.edgeInsets.top)
cellY += self.rowMargin;
attrs.frame = CGRectMake(cellX, cellY, cellWidth, cellHeight);
//更新最短那一列的高度
self.columnHeights[destColumn] = @(CGRectGetMaxY(attrs.frame));
//記錄內(nèi)容的高度(即最長那一列的高度)
CGFloat maxColumnHeight = [self.columnHeights[destColumn] doubleValue];
if (self.contentHeight < maxColumnHeight)
self.contentHeight = maxColumnHeight;
return attrs;
}
獲得屬性數(shù)組之后,在prepareLayout中,對(duì)所有的屬性(高寬、位置、頁邊距)對(duì)應(yīng)的數(shù)組進(jìn)行初始化
//初始化
- (void) prepareLayout
{
[super prepareLayout];
self.contentHeight = 0;
//清除之前計(jì)算的所有高度
[self.columnHeights removeAllObjects];
// 設(shè)置每一列默認(rèn)的高度
for (NSInteger i = 0; i < HobenDefaultColumnCount ; i ++) {
[self.columnHeights addObject:@(HobenDefaultEdgeInsets.top)];
}
// 清除之前所有的布局屬性
[self.attrsArr removeAllObjects];
// 開始創(chuàng)建每一個(gè)cell對(duì)應(yīng)的布局屬性
NSInteger count = [self.collectionView numberOfItemsInSection:0];
for (int i = 0; i < count; i++) {
// 創(chuàng)建位置
NSIndexPath * indexPath = [NSIndexPath indexPathForItem:i inSection:0];
// 獲取indexPath位置上cell對(duì)應(yīng)的布局屬性
UICollectionViewLayoutAttributes * attrs = [self layoutAttributesForItemAtIndexPath:indexPath];
[self.attrsArr addObject:attrs];
}
}
設(shè)置Cell的大小:
//cell的大小
- (NSArray<UICollectionViewLayoutAttributes *> *) layoutAttributesForElementsInRect:(CGRect)rect
{
return self.attrsArr;
}
注意cell和cell之間是有間距的,需要通過計(jì)算,獲得Layout大?。?/p>
//內(nèi)容的大小
- (CGSize) collectionViewContentSize
{
return CGSizeMake(0, self.contentHeight + self.edgeInsets.bottom);
}
3.Controller
Controller是對(duì)每一個(gè)Cell的內(nèi)容屬性(封面圖片等)進(jìn)行設(shè)置,并且獲得每一個(gè)Cell的布局屬性,實(shí)現(xiàn)委托,傳遞給Layout:
首先,一定要記得,有個(gè)ID需要注冊(cè):
static NSString * const HobenCoverId = @"HobenCoverId";
/**
* 創(chuàng)建布局和collectionView
*/
- (void)setupLayoutAndCollectionView
{
// 創(chuàng)建布局
HobenWaterFallLayout * waterFallLayout = [[HobenWaterFallLayout alloc] init];
waterFallLayout.delegate = self;
// 創(chuàng)建collectionView
UICollectionView * collectionView = [[UICollectionView alloc] initWithFrame: self.view.bounds
collectionViewLayout: waterFallLayout];
collectionView.backgroundColor = [UIColor whiteColor];
//設(shè)置DataSource和Delegate
collectionView.dataSource = self;
collectionView.delegate = self;
[self.view addSubview:collectionView];
// 注冊(cè)
[collectionView registerClass: [HobenCoverCell class]
forCellWithReuseIdentifier: HobenCoverId];
self.collectionView = collectionView;
}
解析json:
- (void) refreshCover
{
_isEnd = NO;
[self.collectionView.mj_footer resetNoMoreData];
NSURL *url = [NSURL URLWithString: @"http://*******"];
NSString *jsonString;
jsonString = [NSString stringWithContentsOfURL: url
encoding: NSUTF8StringEncoding
error: nil];
NSData* jsonData;
jsonData = [jsonString dataUsingEncoding: NSUTF8StringEncoding];
//獲得解析的json
_dict = [NSJSONSerialization JSONObjectWithData: jsonData
options: NSJSONReadingMutableContainers
error: nil];
//獲得需要的json數(shù)組
_totalCovers = _dict[@"data"][@"info_list"];
}
在本次項(xiàng)目中,我實(shí)現(xiàn)了加載,因此需要對(duì)獲得的數(shù)據(jù)進(jìn)行分頁,在這里,我設(shè)置每頁有10個(gè)數(shù)據(jù):
/**
* 初始化
*/
- (void)initialize
{
[self refreshCover];
//設(shè)置每頁大小和當(dāng)前頁數(shù)
_sectionNum = 10;
_currentPageNum = 0;
self.title = @"視頻列表";
self.view.backgroundColor = [UIColor whiteColor];
}
刷新控件相應(yīng)的邏輯如下:
/**
* 刷新控件
*/
- (void)setupRefresh
{
self.collectionView.mj_header = [MJRefreshNormalHeader headerWithRefreshingTarget:self refreshingAction:@selector(loadNewCovers)];
self.collectionView.mj_header.backgroundColor = [UIColor whiteColor];
[self.collectionView.mj_header beginRefreshing];
self.collectionView.mj_footer = [MJRefreshAutoNormalFooter footerWithRefreshingTarget:self refreshingAction:@selector(loadMoreShops)];
self.collectionView.mj_footer.backgroundColor = [UIColor whiteColor];
self.collectionView.mj_footer.hidden = YES;
}
下拉刷新,將會(huì)加載新的數(shù)據(jù),其實(shí)現(xiàn)邏輯如下:
/**
* 加載新的視頻
*/
- (void) loadNewCovers
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self refreshCover];
//刷新,當(dāng)前頁數(shù)置0
[_covers removeAllObjects];
_currentPageNum = 0;
int endValue = 0;
//如果到底了
if ((_currentPageNum + 1)* _sectionNum >= [_totalCovers count]) {
endValue = (int)[_totalCovers count] - _currentPageNum * _sectionNum;
}
else {
endValue = _sectionNum;
}
NSArray * cover = [_totalCovers subarrayWithRange: NSMakeRange(_currentPageNum * _sectionNum, endValue)];
[_covers addObjectsFromArray:cover];
// 刷新表格
[self.collectionView reloadData];
[self.collectionView.mj_header endRefreshing];
});
}
上拉加載則需要判斷是否到底,如果到底了,則需要顯示no more data:
//加載更多視頻
- (void) loadMoreCovers
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (_isEnd)
return;
//刷新
_currentPageNum++;
int endValue = 0;
//如果到底了
if ((_currentPageNum + 1)* _sectionNum >= [_totalCovers count]) {
endValue = (int)[_totalCovers count] - _currentPageNum * _sectionNum;
_isEnd = YES;
}
else {
endValue = _sectionNum;
}
NSArray * cover = [_totalCovers subarrayWithRange: NSMakeRange(_currentPageNum * _sectionNum, endValue)];
[_covers addObjectsFromArray:cover];
// 刷新表格
[self.collectionView reloadData];
[self.collectionView.mj_footer endRefreshing];
if (_isEnd)
[self.collectionView.mj_footer endRefreshingWithNoMoreData];
});
}
對(duì)每一個(gè)cell與數(shù)據(jù)一一對(duì)應(yīng),并設(shè)置進(jìn)cell里面:
- (UICollectionViewCell *) collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
HobenCoverCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier: HobenCoverId
forIndexPath: indexPath];
NSDictionary *cover = [[NSDictionary alloc] init];
cover = self.covers[indexPath.item];
/** 圖片 */
NSString *img = cover[@"cover"];
/** 視頻標(biāo)題 */
NSString *title = cover[@"title"];
/** 視頻字段 */
NSString *flv = cover[@"flv"];
/** 視頻時(shí)長 */
NSString *duration = cover[@"duration"];
/** 列表到底 */
NSString *end = cover[@"end"];
/** 寬高 */
NSNumber *height = cover[@"height"];
NSNumber *width = cover[@"width"];
HobenCover *cellCover = [[HobenCover alloc] init];
//設(shè)置內(nèi)容
[cellCover setImg: img];
[cellCover setTitle: title];
[cellCover setFlv: flv];
[cellCover setDuration: duration];
[cellCover setEnd: end];
[cellCover setW: [width floatValue]];
[cellCover setH: [height floatValue]];
cell.cover = cellCover;
return cell;
}
設(shè)置點(diǎn)擊跳轉(zhuǎn)事件:
- (void) collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
HobenCoverCell *cell = (HobenCoverCell *)[collectionView cellForItemAtIndexPath: indexPath];
HobenCover *cover = cell.cover;
HobenVideoController *videoController = [[HobenVideoController alloc] init];
[videoController setCover: cover];
//點(diǎn)擊跳轉(zhuǎn)
[self.navigationController pushViewController: videoController
animated: YES];
}
其他的就不用說了,和之前學(xué)習(xí)的TableView差不多,需要設(shè)置section數(shù)量和section里面的item的數(shù)量:
- (NSInteger) numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
self.collectionView.mj_footer.hidden = self.covers.count == 0;
return self.covers.count;
}
最后實(shí)現(xiàn)委托,傳遞給Layout:
- (CGFloat) waterFallLayout:(HobenWaterFallLayout *)waterFallLayout heightForItemAtIndexPath:(NSUInteger)indexPath itemWidth:(CGFloat)itemWidth
{
NSDictionary *cover = _covers[indexPath];
NSNumber *height = cover[@"height"];
NSNumber *width = cover[@"width"];
return itemWidth / [width floatValue] * [height floatValue];
}
- (CGFloat) rowMarginInWaterFallLayout:(HobenWaterFallLayout *)waterFallLayout
{
return 10;
}
- (NSUInteger) columnCountInWaterFallLayout:(HobenWaterFallLayout *)waterFallLayout
{
return 2;
}
- (UIEdgeInsets) edgeInsetdInWaterFallLayout:(HobenWaterFallLayout *)waterFallLayout
{
return UIEdgeInsetsMake(10, 10, 10, 10);
}
三.實(shí)現(xiàn)視頻播放
這次的視頻播放器使用的是基于AVPlayer封裝的ZFPlayer,不得不說這個(gè)作者真的很強(qiáng)大:
具體參考他的GitHub和博客文檔,配置的話GitHub有說得很完整了。
當(dāng)控制器的view將要布局子控件時(shí),就會(huì)調(diào)用viewWillLayoutSubviews,因此我們首先對(duì)視頻的布局進(jìn)行配置:
- (void) viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
CGFloat w = CGRectGetWidth(self.view.frame);
CGFloat h = w * 9 / 16;
//視頻布局
CGFloat x = 0;
CGFloat y = (CGRectGetHeight(self.view.frame) - h) / 2;
self.containerView.frame = CGRectMake(x, y, w, h);
w = 44;
h = w;
//按鈕居中
x = (CGRectGetWidth(self.containerView.frame) - w) / 2;
y = (CGRectGetHeight(self.containerView.frame) - h) / 2;
self.playBtn.frame = CGRectMake(x, y, w, h);
}
對(duì)于布局加載的各個(gè)函數(shù)的調(diào)用順序,可以看這篇文章
而在控制器的view布局子控件完成時(shí),將調(diào)用viewDidLayoutSubviews,在下面進(jìn)行播放器的初始化:
- (void) initialize
{
//將flv轉(zhuǎn)換成mp4
NSMutableString *flv = [[NSMutableString alloc] initWithString: self.cover.flv];
if (flv == nil)
return;
NSRange range = [flv rangeOfString: @".flv"];
[flv replaceCharactersInRange: range withString: @".mp4"];
_flvReadOnly = [[NSString alloc] initWithString: flv];
//設(shè)置相應(yīng)布局
self.view.backgroundColor = [UIColor whiteColor];
[self.view addSubview:self.containerView];
[self.containerView addSubview:self.playBtn];
ZFAVPlayerManager *playerManager = [[ZFAVPlayerManager alloc] init];
/// 播放器相關(guān)
self.player = [ZFPlayerController playerWithPlayerManager: playerManager
containerView: self.containerView];
self.player.controlView = self.controlView;
//全屏之后,頂部欄隱藏
@weakify(self)
self.player.orientationWillChange = ^(ZFPlayerController * _Nonnull player, BOOL isFullScreen) {
@strongify(self)
[self setNeedsStatusBarAppearanceUpdate];
};
//結(jié)束播放之后停止
self.player.playerDidToEnd = ^(id _Nonnull asset) {
@strongify(self)
if (self.player.isFullScreen) {
[self.player enterFullScreen:NO animated:YES];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.player.orientationObserver.duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.player stop];
});
} else {
[self.player stop];
}
};
}
在播放鍵點(diǎn)擊之后進(jìn)行播放:
- (void) playClick:(UIButton *)sender
{
self.player.assetURL = [NSURL URLWithString: _flvReadOnly];
[self.player playTheIndex: 0];
[self.controlView showTitle: self.cover.title
coverURLString: self.cover.img
fullScreenMode: ZFFullScreenModeLandscape];
}
在加載視頻之前顯示封面圖,注意要先將封面圖進(jìn)行比例壓縮:
- (UIImage*)imageCompressWithSimple:(UIImage*)image scaledToSize:(CGSize)size
{
UIGraphicsBeginImageContext(size);
[image drawInRect:CGRectMake(0,0,size.width,size.height)];
UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
獲得壓縮完畢的封面圖后,我們可以用BackgroundColor的方法,讓視頻未加載前顯示封面圖:
- (UIView *) containerView
{
if (!_containerView) {
_containerView = [UIView new];
NSURL *imageURL = [NSURL URLWithString: self.cover.img];
NSData *data = [NSData dataWithContentsOfURL: imageURL];
UIImage *image = [[UIImage alloc] initWithData: data];
CGFloat w = CGRectGetWidth(self.view.frame);
CGFloat h = w * 9 / 16;
//設(shè)置封面圖的大小
image = [self imageCompressWithSimple: image scaledToSize: CGSizeMake(w, h)];
UIColor *bgColor = [UIColor colorWithPatternImage: image];
[_containerView setBackgroundColor: bgColor];
}
return _containerView;
}
對(duì)播放按鈕進(jìn)行初始化:
- (UIButton *) playBtn
{
if (!_playBtn) {
_playBtn = [UIButton buttonWithType:UIButtonTypeCustom];
[_playBtn setImage:[UIImage imageNamed:@"播放"] forState:UIControlStateNormal];
[_playBtn addTarget:self action:@selector(playClick:) forControlEvents:UIControlEventTouchUpInside];
}
return _playBtn;
}
大功告成,這個(gè)ZFPlayer支持手勢(shì)滑動(dòng)調(diào)節(jié)亮度、音量、進(jìn)度、重力感應(yīng)等功能,可以說是非常強(qiáng)大了。
還有一點(diǎn)就是,使用視頻播放器的時(shí)候不要加斷點(diǎn)!否則會(huì)出現(xiàn)一些很奇怪的錯(cuò)誤,我是看了這篇博客才知道的,真的太坑了。
四.問題反饋與糾正
1.關(guān)于ViewDidLoad、viewDidLayoutSubviews、layoutSubviews的調(diào)用問題
參考文章:UI篇-VC的生命周期以及UIView的layoutSubviews和drawRect方法
首先看看單個(gè)viewController的生命周期:
- loadView:加載view 會(huì)多次調(diào)用并且會(huì)使viewWillLayoutSubviews、viewDidLayoutSubviews不再執(zhí)行
- viewDidLoad:view加載完畢
- viewWillAppear:控制器的view將要顯示
- viewWillLayoutSubviews:控制器的view將要布局子控件
-
viewDidLayoutSubviews:控制器的view布局子控件完成
這期間系統(tǒng)可能會(huì)多次調(diào)用viewWillLayoutSubviews 、 viewDidLayoutSubviews 倆個(gè)方法 - viewDidAppear:控制器的view完全顯示
- viewWillDisappear:控制器的view即將消失的時(shí)候
- viewDidDisappear:控制器的view完全消失的時(shí)候
整個(gè)控制器生命周期: viewDidLoad -> viewWillAppear -> viewWillLayoutSubviews -> viewDidLayoutSubviews -> viewDidAppear -> viewWillDisappear -> viewDidDisappear
viewWillLayoutSubviews 在 viewWillAppear 之后 viewDidAppear 之前執(zhí)行,這個(gè)方法會(huì)被調(diào)用多次,如果在此創(chuàng)建視圖,可能會(huì)創(chuàng)建多個(gè),而且這個(gè)方法中執(zhí)行耗時(shí)操作依然會(huì)造成跳轉(zhuǎn)卡頓的問題。
viewDidLoad是當(dāng)程序第一次加載view時(shí)調(diào)用,以后都不會(huì)用到,而viewDidAppear是每當(dāng)切換到view時(shí)就調(diào)用。
科普完以上知識(shí)之后,再看看我的代碼:
1) viewDidLoad和viewDidLayoutSubviews
//不正確的方法
- (void)viewDidLoad
{
[super viewDidLoad];
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
[self initialize];
}
可以看到,如果創(chuàng)建多個(gè)視圖的話,就會(huì)不斷加載,可以說會(huì)非常消耗了!
2) viewDidLayoutSubviews和viewWillLayoutSubviews
再來看看我的布局代碼放哪了:
- (void) viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
CGFloat w = CGRectGetWidth(self.view.frame);
CGFloat h = w * 9 / 16;
//視頻布局
CGFloat x = 0;
CGFloat y = (CGRectGetHeight(self.view.frame) - h) / 2;
self.containerView.frame = CGRectMake(x, y, w, h);
w = 44;
h = w;
//按鈕居中
x = (CGRectGetWidth(self.containerView.frame) - w) / 2;
y = (CGRectGetHeight(self.containerView.frame) - h) / 2;
self.playBtn.frame = CGRectMake(x, y, w, h);
}
是的,放在了控制器的view將要布局子控件里面,還用到了view的frame!如果我view大小改變了的話,這個(gè)布局就會(huì)不準(zhǔn)確了!
2.關(guān)于使用frame進(jìn)行布局的問題
在Cell的initWithFrame里面,我的布局是這樣的:
[_imageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.contentView).offset(0);
make.size.mas_equalTo(CGSizeMake(self.contentView.bounds.size.width, self.contentView.bounds.size.height * 0.85));
}];
這樣做的問題在于,我是直接獲得了contentView的大小(即寫死了),當(dāng)其布局大小發(fā)生改變的時(shí)候,用mas_equal的方法可不會(huì)隨之而改變。怎么辦?改成這樣:
[_imageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.contentView).offset(0);
make.width.equalTo(self.contentView);
make.height.equalTo(self.contentView).multipliedBy(0.85);
}];
但因?yàn)閛ffset的問題(無法設(shè)置成weight形式),imageView和Label之間還是會(huì)有約束沖突,這時(shí)候我們可以定義一個(gè)空白的View來解決:
//添加空隙
UIView *space = [[UIView alloc] init];
[self.contentView addSubview: space];
[space mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(self.contentView);
make.height.equalTo(self.contentView).multipliedBy(0.04);
}];
這樣就可以按照0.85 : 0.04 : 0.11的比例來控制這個(gè)Cell布局了。
3.關(guān)于網(wǎng)絡(luò)請(qǐng)求與阻塞主線程的問題
在請(qǐng)求數(shù)據(jù)的時(shí)候,我曾經(jīng)是這樣請(qǐng)求的:
- (void) refreshCover
{
_isEnd = NO;
[self.collectionView.mj_footer resetNoMoreData];
NSURL *url = [NSURL URLWithString: @"http://*******"];
NSString *jsonString;
jsonString = [NSString stringWithContentsOfURL: url
encoding: NSUTF8StringEncoding
error: nil];
NSData* jsonData;
jsonData = [jsonString dataUsingEncoding: NSUTF8StringEncoding];
//獲得解析的json
_dict = [NSJSONSerialization JSONObjectWithData: jsonData
options: NSJSONReadingMutableContainers
error: nil];
//獲得需要的json數(shù)組
_totalCovers = _dict[@"data"][@"info_list"];
}
這個(gè)有什么問題呢?問題在于,這個(gè)函數(shù)是寫在了主線程里面,如果加載很久的話,就會(huì)造成阻塞UI的后果。幸運(yùn)的是,AFNetWorking提供了異步請(qǐng)求方法,參考iOS9之后AFNetWorking的使用(詳細(xì)),就可以將代碼修改成這樣:
NSString *URLString = @"http://*******";
//使用AFNetWorking請(qǐng)求
AFHTTPSessionManager *session = [AFHTTPSessionManager manager];
//這一行是解決bug的
session.responseSerializer.acceptableContentTypes=[NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript",@"text/html", @"application/javascript", nil];
//get請(qǐng)求
[session GET: URLString
parameters: nil
progress: nil
success: ^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(@"請(qǐng)求成功");
//獲得字典
_dict = responseObject;
//獲得需要的json數(shù)組
_totalCovers = _dict[@"data"][@"info_list"];
}
failure: ^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"請(qǐng)求失敗:%@", error);
}];
看到那行解決bug沒有,那行好像是因?yàn)檫@個(gè)框架本身的問題導(dǎo)致報(bào)錯(cuò)"Request failed: unacceptable content-type: application/javascript",參考了解決方案。
同樣地,在cell里面,設(shè)置加載中的圖片時(shí),我也直接暴力地請(qǐng)求了網(wǎng)絡(luò):
NSURL *url = [NSURL URLWithString: @"https://image.baidu.com/search/detail?ct=503316480&z=0&ipn=d&word=正在加載圖片gif&hs=2&pn=9&spn=0&di=122397146850&pi=0&rn=1&tn=baiduimagedetail&is=0%2C0&ie=utf-8&oe=utf-8&cl=2&lm=-1&cs=4034065388%2C2568359934&os=124701788%2C3310077975&simid=0%2C0&adpicid=0&lpn=0&ln=30&fr=ala&fm=&sme=&cg=&bdtype=0&oriquery=正在加載圖片gif&objurl=http%3A%2F%2Fimgsrc.baidu.com%2Fforum%2Fw%3D580%2Fsign%3D4fc40444dec451daf6f60ce386fd52a5%2Faef6d933c895d143f95a783970f082025aaf0749.jpg&fromurl=ippr_z2C%24qAzdH3FAzdH3Fptjkwv_z%26e3Bkwt17_z%26e3Bv54AzdH3FrAzdH3Fndb8c999ad%3Frt1%3Dc0al99dn8n0%26fjj_sz%3D8&gsm=0&islist=&querylist="];
NSData *data = [NSData dataWithContentsOfURL: url];
UIImage *image = [UIImage sd_animatedGIFWithData:data];
事實(shí)上,只需要將這個(gè)Gif下載下來,放入Assets文件里面,再這樣加載就OK了:
UIImage *image = [UIImage imageNamed: @"loading"];
(解決失敗,不知道怎么加載gif類型的Placeholder,算了。。)
4.使用官方的UICollectionViewFlowLayout
鑒于自定義Layout太復(fù)雜,在這里還是嘗試一下使用官方的UICollectionViewFlowLayout,好像真的免去了委托等繁雜的工作(不過還是需要計(jì)算,因?yàn)?code>UICollectionViewFlowLayout好像沒有提供列數(shù)這個(gè)接口)
//collectionView的寬度
CGFloat collectionViewWidth = self.view.frame.size.width;
//內(nèi)邊距
UIEdgeInsets edgeInsets = UIEdgeInsetsMake(10, 10, 10, 10);
//列數(shù)
int columnCount = 2;
//列間距
CGFloat columnMargin = 10;
//設(shè)置布局屬性的frame
CGFloat cellWidth = (collectionViewWidth - edgeInsets.left - edgeInsets.right - (columnCount - 1) * columnMargin) / columnCount;
CGFloat cellHeight = cellWidth * 0.8;
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc]init];
//設(shè)置UICollectionViewFlowLayout的屬性
layout.scrollDirection = UICollectionViewScrollDirectionVertical;
layout.sectionInset = edgeInsets;
layout.itemSize = CGSizeMake(cellWidth, cellHeight);
// 創(chuàng)建collectionView
UICollectionView * collectionView = [[UICollectionView alloc] initWithFrame: self.view.bounds
collectionViewLayout: layout];
5.分頁加載與超時(shí)功能的完善
項(xiàng)目給出的HTTP的地址里面有page=和size=,這其實(shí)是用于分頁加載的(之前一直理解錯(cuò)了= =)
所以我們要用占位符來得出加載出來的地址。(這里略)
同時(shí),使用了AFNetworking進(jìn)行了異步加載,則需要處理好MJRefresh控件和AFNetworking邏輯,MJRefresh控件的加載完成必須是在AFNetworking請(qǐng)求完成并且讀取完畢之后才能隱藏:
//get請(qǐng)求
[session GET: URLString
parameters: nil
progress: nil
success: ^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(@"請(qǐng)求成功");
//獲得字典
_dict = responseObject;
//獲得需要的json數(shù)組
_totalCovers = _dict[@"data"][@"info_list"];
//獲得該數(shù)據(jù)請(qǐng)求是否結(jié)束
if ([_dict[@"data"][@"end"] intValue] == 1)
_isEnd = YES;
else
_isEnd = NO;
if ([load isEqualToString: @"loadNewCovers"]) {
//加載成功,更新列表
[_covers addObjectsFromArray: _totalCovers];
[self.collectionView reloadData];
[self.collectionView.mj_header endRefreshing];
}
if ([load isEqualToString: @"loadMoreCovers"]) {
if (_isEnd) {
//加載到底,結(jié)束加載
[self.collectionView.mj_footer endRefreshingWithNoMoreData];
return;
}
else {
//加載成功,更新列表
[_covers addObjectsFromArray: _totalCovers];
[self.collectionView reloadData];
[self.collectionView.mj_footer endRefreshing];
}
}
}
設(shè)置請(qǐng)求的超時(shí)時(shí)間:
[session.requestSerializer willChangeValueForKey: @"timeoutInterval"];
session.requestSerializer.timeoutInterval = 10.0f;
[session.requestSerializer didChangeValueForKey: @"timeoutInterval"];
在網(wǎng)絡(luò)請(qǐng)求失敗的同時(shí),也需要拋出警告視圖。
failure: ^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
//加載失敗,拋出異常
NSLog(@"請(qǐng)求失敗:%@", error);
UIAlertController *alert = [UIAlertController alertControllerWithTitle: @"加載超時(shí)"
message: @"請(qǐng)檢查你的網(wǎng)絡(luò)"
preferredStyle: UIAlertControllerStyleAlert];
UIAlertAction *action = [UIAlertAction actionWithTitle: @"了解"
style: UIAlertActionStyleDefault
handler: nil];
[alert addAction: action];
[self presentViewController: alert
animated: YES
completion: nil];
[self.collectionView.mj_header endRefreshing];
[self.collectionView.mj_footer endRefreshing];
}];
}
為防止下拉加載的時(shí)候多次誤觸控件,需要設(shè)置一個(gè)延時(shí):
//加載更多視頻
- (void) loadMoreCovers
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//刷新
_currentPageNum++;
//開始加載
[self refreshCover: @"loadMoreCovers"];
});
}
自此,分頁加載即可完成了!
6.模擬差網(wǎng)絡(luò)環(huán)境下的刷新
在這里下載網(wǎng)絡(luò)環(huán)境模擬器Network Link Conditioner :
一開始我的footer在差網(wǎng)絡(luò)環(huán)境下不斷上拉就會(huì)不斷刷新currentPageNum,導(dǎo)致UI操作和刷新操作不同步,在這里需要添加一個(gè)邏輯:即在進(jìn)行UI上拉操作后,footer應(yīng)該在UICollectionView加載完成之后,才能繼續(xù)上拉,由此,我加上了這樣一個(gè)操作:dispatch_async(dispatch_get_main_queue())
if ([load isEqualToString: @"loadNewCovers"]) {
//加載成功,更新列表
[_covers addObjectsFromArray: _totalCovers];
[self.collectionView reloadData];
dispatch_async(dispatch_get_main_queue(), ^{
[self.collectionView.mj_header endRefreshing];
});
}
if ([load isEqualToString: @"loadMoreCovers"]) {
if (_isEnd) {
//加載到底,結(jié)束加載
[self.collectionView.mj_footer endRefreshingWithNoMoreData];
return;
}
else {
//加載成功,更新列表
[_covers addObjectsFromArray: _totalCovers];
[self.collectionView reloadData];
dispatch_async(dispatch_get_main_queue(), ^{
[self.collectionView.mj_footer endRefreshing];
});
}
}
加上以后,即可實(shí)現(xiàn)我想要的邏輯。同時(shí)記得下拉刷新后resetNoMoreData:
if ([load isEqualToString: @"loadNewCovers"]) {
[_covers removeAllObjects];
[self.collectionView.mj_footer resetNoMoreData];
}