iOS9 UICollectionView拖拽移动单元格

iOS9 UICollectionView拖拽移动单元格

浏览:4046

  近日因为系统升级导致xcode6.系列版本出现bug,于是开始使用xcode7。在使用之余突然想到collectionView在iOS9中发布了一个可以移动cell的新特性,就尝试着将其实现,无奈api文档接口无法查看,只有一些列的api放在那里。于是上网查找,发现国内没有搜索到此类文章,于是FQ继续找,最终找到的竟然都是swift版本,于是将其转换为oc版本以帮助国内需要的朋友学习使用。下面是具体用法:

1.创建collectionView并设置代理

 
- (UICollectionView *)collectionView{
    if (_collectionView == nil) {
        UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
        layout.itemSize = CGSizeMake(50, 50);
        _collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 20, self.view.bounds.size.width, self.view.bounds.size.height) collectionViewLayout:layout];
        layout.minimumLineSpacing = 10;
        layout.minimumInteritemSpacing = 10;
        [_collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"Cell"];
        _collectionView.backgroundColor = [UIColor cyanColor];
        _collectionView.dataSource = self;
       //此处给其增加长按手势,用此手势触发cell移动效果
        UILongPressGestureRecognizer *longGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handlelongGesture:)];
        [_collectionView addGestureRecognizer:longGesture];
    }
    return _collectionView;
}
 

2.设置其资源

_dataSource = [NSMutableArray array];
    for (int i = 1; i <= 50; i++) {
        NSString *imageName = [NSString stringWithFormat:@"%d",i];
        [_dataSource addObject:imageName];
    }

3.监听手势,并设置其允许移动cell和交换资源

 

 
- (void)handlelongGesture:(UILongPressGestureRecognizer *)longGesture {
    //判断手势状态
    switch (longGesture.state) {
        case UIGestureRecognizerStateBegan:{
            //判断手势落点位置是否在路径上
            NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:[longGesture locationInView:self.collectionView]];
            if (indexPath == nil) {
                break;
            }
            //在路径上则开始移动该路径上的cell
            [self.collectionView beginInteractiveMovementForItemAtIndexPath:indexPath];
        }
            break;
        case UIGestureRecognizerStateChanged:
            //移动过程当中随时更新cell位置
            [self.collectionView updateInteractiveMovementTargetPosition:[longGesture locationInView:self.collectionView]];
            break;
        case UIGestureRecognizerStateEnded:
            //移动结束后关闭cell移动
            [self.collectionView endInteractiveMovement];
            break;
        default:
            [self.collectionView cancelInteractiveMovement];
            break;
    }
}

- (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath{
    //返回YES允许其item移动
    return YES;
}

- (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath*)destinationIndexPath {
    //取出源item数据
    id objc = [_dataSource objectAtIndex:sourceIndexPath.item];
    //从资源数组中移除该数据
    [_dataSource removeObject:objc];
    //将数据插入到资源数组中的目标位置上
    [_dataSource insertObject:objc atIndex:destinationIndexPath.item];
} 
 

 

通过以上设置便可以成功移动cell了,下面奉上效果图

至此collectionView的新特性使用方法展示完成。


 

 

UICollectionView和UITableView很类似,不过对于我个人来讲,UITableView是经常用到的东西,UICollectionView使用较少,所以这篇文章讲UICollectionView。

1.类和协议

1).UICollectionViewController:与UITableViewController功能类似
2).UICollectionViewCell:与UITableViewCell功能类似,同样有ReuseIdentifier,所以它也有复用机制。

从storyBoard中出列:

MyCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"myCell" forIndexPath:indexPath];
cell.cellLabel.text = [NSString stringWithFormat:@"%ld",(long)indexPath.item];

从nib中注册:

[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:reuseIdentifier];

3).UICollectionViewDataSource:数据源协议

4).UICollectionViewDelegate:处理包含选中事件的各种方法的协议

5).UICollectionViewDelegateFlowLayout:这是UICollectionView和UITableView不同的地方,它可以用来定制一些布局。

2.例子

1).初始化

新建一个工程,删除ViewController类,将storyBoard中的ViewController替换为UICollectionViewController。
像往常一样,你的主要内容显示在 cell 中,cell 可以被任意分组到 section 中。Collection view 的 cell 必须是 UICollectionViewCell 的子类。所以我们新建UICollectionViewController与UICollectionViewCell的子类,将storyBoard中UICollectionViewController的custom class设置为MyCollectionViewController,将UICollectionViewCell的custom class设置为MyCollectionViewCell。

在UICollectionViewCell中新增如下图两个控件,UIImageView和UILabel

不要忘记设置cell的Identifier:

建立两个IBOutlet:

2).实现数据源方法

MyCollectionViewController.m:

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView{
    return 1;
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
    return 20;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
    MyCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"myCell" forIndexPath:indexPath];
    cell.cellLabel.text = [NSString stringWithFormat:@"%ld",(long)indexPath.item];

    return cell;
}

配置cell,MyCollectionViewCell.m

-(void)awakeFromNib{
    [super awakeFromNib];
    self.backgroundColor = [UIColor randomColor];

}

现在运行,如下图:

IMG_0939.PNG
IMG_0939.PNG

旋转屏幕后栅格会自动旋转并对齐:

IMG_0940.PNG
IMG_0940.PNG

2).实现委托方法

a.高亮

在cell中添加一个selectedBackgroundView视图:

-(void)awakeFromNib{
    [super awakeFromNib];
    self.selectedBackgroundView = [[UIView alloc]initWithFrame:self.frame];
    self.selectedBackgroundView.backgroundColor = [UIColor blackColor];

    self.backgroundColor = [UIColor randomColor];
}

实现以下代理方法:

-(BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath{
    return YES;
}

//放大缩小效果
-(void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath{
    UICollectionViewCell *selectedCell = [collectionView cellForItemAtIndexPath:indexPath];
    [UIView animateWithDuration:kAnimationDuration animations:^{
        selectedCell.transform = CGAffineTransformMakeScale(2.0f, 2.0f);
    }];
}

-(void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath{
    UICollectionViewCell *selectedCell = [collectionView cellForItemAtIndexPath:indexPath];
    [UIView animateWithDuration:kAnimationDuration animations:^{
        selectedCell.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
    }];
}

现在按下collectionCell会显示高亮状态:背景颜色变黑色,且有一个弹跳的放大缩小效果。

b.选中

如上右边新建一个MyDetailsViewController,并且从左边控制器中segue到MyDetailsViewController。

MyDetailsViewController.m

-(IBAction) doneTapped:(id) sender {
  [self dismissViewControllerAnimated:YES completion:nil];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.imageView.image = [UIImage imageNamed:@"image"];
}

实现以下代理方法:
MyCollectionViewController.m

-(BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath{
    return YES;
}

-(void) collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
    dispatch_time_t delayInNanoSeconds = dispatch_time(DISPATCH_TIME_NOW, (int64_t)1*NSEC_PER_SEC);
    dispatch_after(delayInNanoSeconds, dispatch_get_main_queue(), ^{
        [self performSegueWithIdentifier:@"MainSegue" sender:indexPath];
    });
}

这样在高亮效果1秒后会进行视图切换。

4).添加头部和尾部视图

collection view 额外管理着两种视图:supplementary views , Supplementary views 相当于 table view 的 section header 和 footer views。像 cells 一样,他们的内容都由数据源对象驱动。然而和 table view 中用法不一样,supplementary view 并不一定会作为 header 或 footer view;他们的数量和放置的位置完全由布局控制。

Supplementary views必须是 UICollectionReusableView的子类。布局使用的每个视图类都需要在 collection view 中注册,这样当 data source 让它们从 reuse pool 中出列时,它们才能够创建新的实例。首先我们需要在storyBoard中启用"Section Header"和"Section Footer"

之后XCode会自动生成两个UICollectionResuableView到视图中:

然后同样的你可以设置Identifier,然后在以下代理方法中dequeue即可,确实很分别,相比UITableView又进一步封装。

-(UICollectionReusableView *)collectionView:(UICollectionView *)collectionView
          viewForSupplementaryElementOfKind:(NSString *)kind
                                atIndexPath:(NSIndexPath *)indexPath{
    NSString *resueIndentifier = kCollectionViewHeaderIndentifier;
    UICollectionReusableView *view = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:resueIndentifier forIndexPath:indexPath];
    return  [collectionView dequeueReusableSupplementaryViewOfKind: UICollectionElementKindSectionFooter
                                               withReuseIdentifier:SupplementaryViewIdentifier
                                                      forIndexPath:indexPath];
}

在这个demo,我演示下如何通过加载自定义的nib控件来添加头部和尾部视图,如下我们新建两个自定义nib控件:

MyCollectionViewController中加载并注册nib:

-(void)awakeFromNib{
    UINib *headerNib = [UINib nibWithNibName:NSStringFromClass([Header class]) bundle:[NSBundle mainBundle]];
    [self.collectionView registerNib:headerNib forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:kCollectionViewHeaderIndentifier];

    UINib *footerNib = [UINib nibWithNibName:NSStringFromClass([Footer class]) bundle:[NSBundle mainBundle]];
    [self.collectionView registerNib:footerNib forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:kCollectionViewFooterIndentifier];
}

代理方法类似:

-(UICollectionReusableView *)collectionView:(UICollectionView *)collectionView
          viewForSupplementaryElementOfKind:(NSString *)kind
                                atIndexPath:(NSIndexPath *)indexPath{
    NSString *resueIndentifier = kCollectionViewHeaderIndentifier;
    if ([kind isEqualToString:UICollectionElementKindSectionFooter]) {
        resueIndentifier = kCollectionViewFooterIndentifier;
    }

    UICollectionReusableView *view = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:resueIndentifier forIndexPath:indexPath];
    if ([kind isEqualToString:UICollectionElementKindSectionHeader]) {
        Header *header = (Header *)view;
        header.label.text = [NSString stringWithFormat:@"Section Header %lu",(unsigned long)indexPath.section+1];
    }else if ([kind isEqualToString:UICollectionElementKindSectionFooter]){
        Footer *footer = (Footer *)view;
        NSString *title = [NSString stringWithFormat:@"Section Footer %lu",(unsigned long)indexPath.section+1];
        [footer.button setTitle:title forState:UIControlStateNormal];
    }
    return  view;    
}

UICollectionView和UITableView最重要的区别就是UICollectionView并不知道如何布局,它把布局机制委托给了UICollectionViewLayout子类,默认的布局方式是UICollectionFlowViewLayout类提供的流式布局(flow layout),也就是上面例子显示的那样子。这个类允许你通过UICollectionDelegateViewFlowLayout协议调整各自属性。

不过你也可以创建自己的布局方式,通过继承UICollectionViewLayout,现在是一个例子。

3.UICollectionViewLayout子类

上面的例子中,我们所有cell的大小都是一样的,那如果我们的cell大小不一样呢?我们需要实现UICollectionViewDelegateFlowLayout的协议方法collectionView:layout:sizeForItemAtIndexPath:,但这会使得效果就像下面左边那张图。它会计算每一排中的最大高度,这样会让效果看起来不怎么样。我们可以继承UICollectionViewLayout来实现右图中的效果。

我们新建一个UICollectionViewController,并把程序运行开始移到改控制器。

像上面的例子那样,显示50个同样大小的单元,具体上面已经介绍了,之后它看起来像这样:

现在实现UICollectionViewDelegateFlowLayout的协议方法随机改变cell大小的高度:

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {    
    CGFloat randomHeight = 80 + (arc4random() % 150);
    return CGSizeMake(80, randomHeight);
}

现在效果是这样:

现在创建一个UICollectionViewLayout的子类:CustomCollectionViewLayout.首先我们需要像UICollectionViewDelegateFlowLayout一样通过代理的方式来获取特定indexPath上cell的高度。

@class CustomCollectionViewLayout;

@protocol CustomCollectionViewLayoutDelegate <NSObject>
@required
- (CGFloat) collectionView:(UICollectionView*) collectionView
                    layout:(CustomCollectionViewLayout*) layout
  heightForItemAtIndexPath:(NSIndexPath*) indexPath;
@end

子类需要覆盖父类以下3个方法:

-(void) prepareLayout;
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect;
-(CGSize) collectionViewContentSize;

prepareLayout在布局开始之前会被调用,我们需要在这个方法中计算边框,所以我们引入numberOfColumns 和 interItemSpacing两个变量。分别是item每行的数目和item间的间距,所以头文件如下:

@interface CustomCollectionViewLayout : UICollectionViewLayout

@property (nonatomic, assign) NSUInteger numberOfColumns;
@property (nonatomic, assign) CGFloat interItemSpacing;
@property (weak, nonatomic) id<CustomCollectionViewLayoutDelegate> delegate;

@end

在开始布局前会执行的方法prepareLayout中,我们需要计算每个item的frame值,并把它存入字典layoutInfo中,然后,在我们覆盖父类的方法layoutAttributesForElementsInRect中,可以返回这个字典中的全部frame总值的数组。

在prepareLayout中,frame的height可以通过代理传入:

CGFloat height = [((id<CustomCollectionViewLayoutDelegate>)self.collectionView.delegate)
                              collectionView:self.collectionView
                              layout:self
                              heightForItemAtIndexPath:indexPath];

frame的width则和numberOfColumns 和 interItemSpacing有关,如下:

//计算Item的宽度
    CGFloat fullWidth = self.collectionView.frame.size.width;
    CGFloat availableSpaceExcludingPadding = fullWidth - (self.interItemSpacing * (self.numberOfColumns + 1));
    CGFloat itemWidth = availableSpaceExcludingPadding / self.numberOfColumns;

x轴和y轴则和当前的indexPath有关,所以我们遍历section和item,得到x轴和y轴,并将之前的高度和宽度加起来得到frame值。

    NSIndexPath *indexPath;
    NSInteger numSections = [self.collectionView numberOfSections];
    //遍历section
    for(NSInteger section = 0; section < numSections; section++)  {
        NSInteger numItems = [self.collectionView numberOfItemsInSection:section];
        //遍历item
        for(NSInteger item = 0; item < numItems; item++){
            indexPath = [NSIndexPath indexPathForItem:item inSection:section];
            UICollectionViewLayoutAttributes *itemAttributes =
            [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];  
            //计算x轴
            CGFloat x = self.interItemSpacing + (self.interItemSpacing + itemWidth) * currentColumn;
            //计算y轴
            CGFloat y = [self.lastYValueForColumn[@(currentColumn)] doubleValue];
            //通过协议回传高度值
            CGFloat height = [((id<CustomCollectionViewLayoutDelegate>)self.collectionView.delegate)
                              collectionView:self.collectionView
                              layout:self
                              heightForItemAtIndexPath:indexPath];
            itemAttributes.frame = CGRectMake(x, y, itemWidth, height);
            //下一个item的y轴是当前y轴加上item高度,并且加上间距
            y += height;
            y += self.interItemSpacing;

            //把下一个item的y轴记入到字典中
            self.lastYValueForColumn[@(currentColumn)] = @(y);

            currentColumn ++;
            if(currentColumn == self.numberOfColumns) currentColumn = 0;
            //将item的属性记录到字典中
            self.layoutInfo[indexPath] = itemAttributes;
        }
    }

然后在我们需要覆盖的第二个方法中,使用enumerateKeysAndObjectsUsingBlock遍历prepareLayout中的layoutInfo加入一个数组中:

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSMutableArray *allAttributes = [NSMutableArray arrayWithCapacity:self.layoutInfo.count];
    [self.layoutInfo enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath,
                                                         UICollectionViewLayoutAttributes *attributes,
                                                         BOOL *stop) {
        if (CGRectIntersectsRect(rect, attributes.frame)) {
            [allAttributes addObject:attributes];
        }
    }];
    return allAttributes;
}

最后一个方法是计算collectionView的内容大小,在第一个方法中,我们已经把下每个item的y轴记入到字典lastYValueForColumn中,所以我们通过do-while循环把这个最大的y值给取出来,加上宽度值即可返回collectionView的内容大小。

-(CGSize) collectionViewContentSize {
    NSUInteger currentColumn = 0;
    CGFloat maxHeight = 0;
    do {
        //最大高度就是之前字典中的y轴
        CGFloat height = [self.lastYValueForColumn[@(currentColumn)] doubleValue];
        if(height > maxHeight)
            maxHeight = height;
        currentColumn ++;
    } while (currentColumn < self.numberOfColumns);

    return CGSizeMake(self.collectionView.frame.size.width, maxHeight);
}

Done!运行下效果如何:

你可以在这里下载到本文的代码。


频道:iOS