版本
Xcode 9.1
##一、布局结构
与UITableView很类似,UICollectionView也由组头、组尾、cell来组成:
其中,①②③④分别是cell在CollectionView里面的上下左右边界缩进,⑤是cell与cell之间的列间间隔,⑥是cell与cell之间的行间间隔。
##二、UICollectionView的创建
###1、示例代码
#define VIEW_WIDTH [UIScreen mainScreen].bounds.size.width
#define VIEW_HEIGHT [UIScreen mainScreen].bounds.size.height
#import "ViewController.h"
@interface ViewController () <UICollectionViewDelegateFlowLayout, UICollectionViewDelegate, UICollectionViewDataSource>
@property (nonatomic, strong) UICollectionView *collectionView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// 添加collectionView
[self.view addSubview:self.collectionView];
}
#pragma mark - UICollectionViewDelegateFlowLayout
// 返回Header的尺寸大小
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section {
return CGSizeMake(VIEW_WIDTH, 33);
}
// 返回cell的尺寸大小
-(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return CGSizeMake(50, 50); // 让每个cell尺寸都不一样
}
// 返回Footer的尺寸大小
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section {
return CGSizeMake(VIEW_WIDTH, 33);
}
// 返回cell之间行间隙
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section {
return 10;
}
// 返回cell之间列间隙
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section {
return 20;
}
// 设置上左下右边界缩进
-(UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
return UIEdgeInsetsMake(20, 20, 20, 20);
}
#pragma mark - UICollectionViewDataSource
// 返回Section个数
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return 2;
}
// 返回cell个数
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return 10;
}
// 返回cell内容
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
// 创建cell (重用)
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"MyCell" forIndexPath:indexPath];
// 设置cell
if (indexPath.section == 0) {
cell.backgroundColor = [UIColor purpleColor];
}else {
cell.backgroundColor = [UIColor darkGrayColor];
}
UILabel *label = [[UILabel alloc] initWithFrame:cell.bounds];
label.textAlignment = NSTextAlignmentCenter;
label.text = @"Cell";
label.textColor = [UIColor whiteColor];
[cell addSubview:label];
return cell;
}
// 返回Header/Footer内容
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
if ([kind isEqualToString:UICollectionElementKindSectionHeader]) { // Header视图
// 从复用队列中获取HooterView
UICollectionReusableView *headerView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"MyHeader" forIndexPath:indexPath];
// 设置HooterView
headerView.backgroundColor = [UIColor redColor];
UILabel *label = [[UILabel alloc] initWithFrame:headerView.bounds];
label.textAlignment = NSTextAlignmentCenter;
label.text = @"我是Header";
label.textColor = [UIColor whiteColor];
[headerView addSubview:label];
// 返回HooterView
return headerView;
}else if ([kind isEqualToString:UICollectionElementKindSectionFooter]) { // Footer视图
// 从复用队列中获取FooterView
UICollectionReusableView *footerView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"MyFooter" forIndexPath:indexPath];
// 设置FooterView
footerView.backgroundColor = [UIColor blueColor];
UILabel *label = [[UILabel alloc] initWithFrame:footerView.bounds];
label.textAlignment = NSTextAlignmentCenter;
label.text = @"我是Footer";
label.textColor = [UIColor whiteColor];
[footerView addSubview:label];
// 返回FooterView
return footerView;
}
return nil;
}
#pragma mark - UICollectionViewDelegate
// 选中某个cell
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
NSLog(@"选中cell: %ld", indexPath.row);
}
#pragma mark - 懒加载
- (UICollectionView *)collectionView {
if (_collectionView == nil) {
// 创建FlowLayout
UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
// 垂直方向滑动
flowLayout.scrollDirection = UICollectionViewScrollDirectionVertical;
// 创建collectionView
CGRect frame = CGRectMake(0, 0, VIEW_WIDTH, VIEW_HEIGHT);
_collectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:flowLayout];
// 设置代理
_collectionView.delegate = self;
_collectionView.dataSource = self;
// 其他属性
_collectionView.backgroundColor = [UIColor clearColor];
_collectionView.showsVerticalScrollIndicator = NO; // 隐藏垂直方向滚动条
// 注册cell
[_collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"MyCell"];
// 注册Header
[_collectionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"MyHeader"];
// 注册Footer
[_collectionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"MyFooter"];
}
return _collectionView;
}
@end
###2、自定义cell(3种创建方式)
####a、纯代码创建cell
先在创建collectionView的时候注册cell,再在cellForItemAtIndexPath方法中复用cell。
// 注册cell
[_collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"MyCell"];
// 然后在cellForItemAtIndexPath方法中重用
// 创建cell (重用)
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"MyCell" forIndexPath:indexPath];
注:在UITableView中可以不提前注册cell,然后cellForRowAtIndexPath方法中判断cell为空时新建一个cell
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"cell"];
}
但是在UICollectionView中,一定要先注册cell才能使用(UICollectionViewCell是没有init方法的)。
####b、使用XIB创建cell
然后先注册:
// 注册XIB cell
[_collectionView registerNib:[UINib nibWithNibName:@"MyXIBCell" bundle:nil] forCellWithReuseIdentifier:@"MyXIBCell"];
再使用:
// 创建cell (重用)
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"MyXIBCell" forIndexPath:indexPath];
####c、使用storyboard创建cell
使用storyboard无需注册cell,因在创建的时候cell已经被关联到相应的collectionView里了,系统在创建collectionView的时候已经帮我们注册cell了,我们只需要设置storyboard里cell的Identifier,然后直接复用就好啦
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"MyStoryboardCell" forIndexPath:indexPath];
###三、流布局、数据源和代理方法
####1、流布局 UICollectionViewDelegateFlowLayout
// cell行间的间距最小距离
@property (nonatomic) CGFloat minimumLineSpacing;
// cell列间的间距最小距离
@property (nonatomic) CGFloat minimumInteritemSpacing;
// 每个item(cell)的大小
@property (nonatomic) CGSize itemSize;
// 每个Item(cell)的预估大小
@property (nonatomic) CGSize estimatedItemSize NS_AVAILABLE_IOS(8_0);
// 滚动方向(默认垂直方向)
@property (nonatomic) UICollectionViewScrollDirection scrollDirection;
// 组头视图大小
@property (nonatomic) CGSize headerReferenceSize;
// 组尾视图大小
@property (nonatomic) CGSize footerReferenceSize;
// cells的上下左右边界缩进
@property (nonatomic) UIEdgeInsets sectionInset;
// 组头视图是否固定在屏幕上边
@property (nonatomic) BOOL sectionHeadersPinToVisibleBounds NS_AVAILABLE_IOS(9_0);
// 组尾视图是否固定在屏幕下边
@property (nonatomic) BOOL sectionFootersPinToVisibleBounds NS_AVAILABLE_IOS(9_0);
/* 以下均为代理方法 */
// 动态设置每个Item(cell)的尺寸大小
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;
// 动态设置cells的上下左右边界缩进
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section;
// 动态设置cell行间的间距最小距离
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section;
// 动态设置cell列间的间距最小距离
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section;
// 动态设置组头视图大小
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section;
// 动态设置组尾视图大小
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section;
####2、数据源 UICollectionViewDataSource
@required 必须实现
// 每组中cell的个数
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section;
// 返回cell (cell必须使用dequeueReusableCellWithReuseIdentifier:forIndexPath:方法复用)
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath;
@optional 可选实现
// 设置组数(默认1组)
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView;
// 返回组头/组尾视图(必须使用dequeueReusableSupplementaryViewOfKind:withReuseIdentifier:forIndexPath:方法复用)
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath;
// 设置某个cell是否可以移动
- (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(9_0);
// 移动cell的时候,会调用这个方法
- (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath*)destinationIndexPath NS_AVAILABLE_IOS(9_0);
####3、代理方法 UICollectionViewDelegate
// 设置某个cell是否可以高亮
- (BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath;
// 某cell高亮时调用此方法
- (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath;
// 某cell结束高亮时调用此方法
- (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath;
// 设置某个cell是否可以选中
- (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath;
// 设置某个cell是否可以取消选中
- (BOOL)collectionView:(UICollectionView *)collectionView shouldDeselectItemAtIndexPath:(NSIndexPath *)indexPath;
// 选中某个cell时调用此方法
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath;
// 取消选中某个cell时调用此方法
- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath;
// 将要显示某个cell时调用此方法
- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(8_0);
// 将要显示某个组头/组尾视图时调用此方法
- (void)collectionView:(UICollectionView *)collectionView willDisplaySupplementaryView:(UICollectionReusableView *)view forElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(8_0);
// 已经显示某个cell时调用此方法
- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath;
// 已经显示某个组头/组尾视图时调用此方法
- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingSupplementaryView:(UICollectionReusableView *)view forElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath;
// 设置是否展示长按cell菜单
- (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath;
// 设置要展示的菜单选项
- (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender;
// 点击菜单按钮后调用此方法(只有copy、cut和paste三个方法可用)
- (void)collectionView:(UICollectionView *)collectionView performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender;
// collectionView重新布局时调用此方法
- (nonnull UICollectionViewTransitionLayout *)collectionView:(UICollectionView *)collectionView transitionLayoutForOldLayout:(UICollectionViewLayout *)fromLayout newLayout:(UICollectionViewLayout *)toLayout;
四、遇到的问题
1. 当UICollectionViewCell只有一个时,系统自动设置成居中对齐
如图:
一般的,我们想让所有section中的cell左对齐,如下:
有两种方法:
法一:
可以使用私有方法,使用运行时机制:
#import <objc/message.h>
- (void)viewDidLoad {
[super viewDidLoad];
// cell只剩下一个时自动左对齐
SEL sel = NSSelectorFromString(@"_setRowAlignmentsOptions:");
if ([self.collectionView.collectionViewLayout respondsToSelector:sel]) {
((void(*)(id,SEL,NSDictionary*))objc_msgSend)(self.collectionView.collectionViewLayout, sel,
@{@"UIFlowLayoutCommonRowHorizontalAlignmentKey":@(NSTextAlignmentLeft),
@"UIFlowLayoutLastRowHorizontalAlignmentKey" : @(NSTextAlignmentLeft),
@"UIFlowLayoutRowVerticalAlignmentKey" : @(NSTextAlignmentCenter)});
}
}
用此私有方法目前可以上架审核通过。
法二:
强制cell居左。
原理是判断section中剩下最后一个cell时,设置右边距足够大,使得左边显示部分只剩下一个cell的宽度:
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
if (section < self.groups.count) {
if (self.groups[section].count == 1) {
return UIEdgeInsetsMake(0, 0, 0, collectionView.frame.size.width-60); // 60是我的固定cell宽
}
}
return UIEdgeInsetsMake(0, 0, 0, 0);
}
这种方法计算麻烦,但没有使用苹果的私有方法,不会有上架被拒风险。