iOS 可拖拽排序的UICollectionView

文章介绍了如何通过创建SDMovableCellCollectionView来实现类似系统Reminder的拖拽排序功能,包括长按手势识别、移动事件代理方法、手势开始和结束时的操作,以及边缘滚动等特殊情况处理。提供了Demo的Git地址供参考。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

系统的UICollectionView和UITableView有提供在编辑态拖拽排序的功能,但是需要指定的EditStyle,而且还只能拖动排序按钮区域,大部分时候效果很难满足业务需求,因此我们需要定制可拖拽Cell的SDMovableCellCollectionView,拖拽效果类似于系统Reminder。文章最后附有Demo的Git地址,可查看完整代码,如有疑问和不妥,评论区交流。

实现思路
  1. 首先要创建CollectionView,继承UICollectionView,声明移动事件触发代理方法,添加长按手势;
@protocol SDMovableCellCollectionViewDelegate <UICollectionViewDelegate>
@optional
/**
 *  The cell that will start moving the indexPath location
 *  长按拖拽cell将要开始移动
 */
- (void)collectionView:(SDMovableCellCollectionView *)collectionView willMoveCellAtIndexPath:(NSIndexPath *)indexPath;
/**
 *  Move cell `fromIndexPath` to `toIndexPath` completed
 *  移动cell换了位置下标
 */
- (void)collectionView:(SDMovableCellCollectionView *)collectionView didMoveCellFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath;
/**
 *  Move cell ended
 *  移动cell结束
 */
- (void)collectionView:(SDMovableCellCollectionView *)collectionView endMoveCellAtIndexPath:(NSIndexPath *)indexPath;

/**
 *  The user tries to move a cell that is not allowed to move. You can make some prompts to inform the user.
 *  尝试长按拖拽不能移动的cell 这个代理方法可以加个toast或者其他处理 与设置`canHintWhenCannotMove`不冲突
 */
- (void)collectionView:(SDMovableCellCollectionView *)collectionView tryMoveUnmovableCellAtIndexPath:(NSIndexPath *)indexPath;

/**
 *  Customize the screenshot style of the movable cell
 *  自定义移动的cell截图的样式 加阴影啥的
 */
- (void)collectionView:(SDMovableCellCollectionView *)collectionView customizeMovalbeCell:(UIImageView *)movableCellsnapshot;

/**
 *  Custom start moving cell animation
 *  自定义cell拖拽移动的动画
 */
- (void)collectionView:(SDMovableCellCollectionView *)collectionView customizeStartMovingAnimation:(UIImageView *)movableCellsnapshot fingerPoint:(CGPoint)fingerPoint;

@end

- (void)addGesture {
    _longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(processGesture:)];
    _longPressGesture.minimumPressDuration = 0.5f;
    [self addGestureRecognizer:_longPressGesture];
}
  1. 然后手势开始时,找到当前手势坐标所在的startIndexPath,对cell生成snapshot,并隐藏此cell,接下来手势改变时都是改变snapshot在CollectionView中的位置;
- (void)gestureBegan:(UILongPressGestureRecognizer *)gesture {
    CGPoint point = [gesture locationInView:gesture.view];
    NSIndexPath *selectedIndexPath = [self indexPathForItemAtPoint:point];
    if (!selectedIndexPath) {
        return;
    }
    
    UICollectionViewCell *cell = [self cellForItemAtIndexPath:selectedIndexPath];
    cell.alpha = 1.0;

    if (self.dataSource && [self.dataSource respondsToSelector:@selector(collectionView:canMoveItemAtIndexPath:)]) {
        if (![self.dataSource collectionView:self canMoveItemAtIndexPath:selectedIndexPath]) {
            //It is not allowed to move the cell, then shake it to prompt the user.
            if (self.canHintWhenCannotMove) {
                CAKeyframeAnimation *shakeAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.translation.x"];
                shakeAnimation.duration = 0.25;
                shakeAnimation.values = @[@(-20), @(20), @(-10), @(10), @(0)];
                [cell.layer addAnimation:shakeAnimation forKey:@"shake"];
            }

            if (self.delegate && [self.delegate respondsToSelector:@selector(collectionView:tryMoveUnmovableCellAtIndexPath:)]) {
                [self.delegate collectionView:self tryMoveUnmovableCellAtIndexPath:selectedIndexPath];
            }
            
            UICollectionViewCell *cell = [self cellForItemAtIndexPath:selectedIndexPath];
            cell.alpha = 0.7;
            return;
        }
    }

    if (self.delegate && [self.delegate respondsToSelector:@selector(collectionView:willMoveCellAtIndexPath:)]) {
        [self.delegate collectionView:self willMoveCellAtIndexPath:selectedIndexPath];
    }
    if (_canEdgeScroll) {
        [self startEdgeScroll];
    }
    //Get a data source every time you move
    if (self.dataSource && [self.dataSource respondsToSelector:@selector(dataSourceArrayInCollectionView:)]) {
        _tempDataSource = [self.dataSource dataSourceArrayInCollectionView:self];
    }
    _selectedIndexPath = selectedIndexPath;
    _startIndexPath = selectedIndexPath;

    if (self.canFeedback) {
        [self.generator prepare];
        [self.generator impactOccurred];
    }
    
    if (self.dataSource && [self.dataSource respondsToSelector:@selector(snapshotViewWithCell:)]) {
        UIView *snapView = [self.dataSource snapshotViewWithCell:cell];
        _snapshot = [self snapshotViewWithInputView:snapView];
    } else {
        _snapshot = [self snapshotViewWithInputView:cell];
    }
    
    if (self.delegate && [self.delegate respondsToSelector:@selector(collectionView:customizeMovalbeCell:)]) {
        [self.delegate collectionView:self customizeMovalbeCell:_snapshot];
    } else {
        _snapshot.layer.shadowColor = [UIColor grayColor].CGColor;
        _snapshot.layer.masksToBounds = NO;
        _snapshot.layer.cornerRadius = 0;
        _snapshot.layer.shadowOffset = CGSizeMake(-5, 0);
        _snapshot.layer.shadowOpacity = 0.4;
        _snapshot.layer.shadowRadius = 5;
    }
    
//    _snapshot.frame = CGRectMake((cell.frame.size.width - _snapshot.frame.size.width)/2.0f, cell.frame.origin.y + (cell.frame.size.height - _snapshot.frame.size.height)/2.0, _snapshot.frame.size.width, _snapshot.frame.size.height);
    _snapshot.frame = cell.frame;
    [self addSubview:_snapshot];
    
    // 记录手势中心偏移
    self.gesturePointOffsetX = point.x - _snapshot.center.x;
    self.gesturePointOffsetY = point.y - _snapshot.center.y;

    cell.hidden = YES;
    if (self.delegate && [self.delegate respondsToSelector:@selector(collectionView:customizeStartMovingAnimation:fingerPoint:)]) {
        [self.delegate collectionView:self customizeStartMovingAnimation:_snapshot fingerPoint:point];
    } else {
        [UIView animateWithDuration:0.3 animations:^{
            self.snapshot.transform = CGAffineTransformScale(self.snapshot.transform, 1.1, 1.1);
        }];
    }
}
  1. 手势改变时,snapshot的center跟随手势坐标变化,因为手势触发位置不一定是cell的center,所以要计算中心偏移,获取手势坐标所在的indexPath,并和startIndexPath交换位置;
- (void)gestureChanged:(UILongPressGestureRecognizer *)gesture {
    CGPoint point = [gesture locationInView:gesture.view];
    point.x -= self.gesturePointOffsetX;
    point.y -= self.gesturePointOffsetY;
    point = CGPointMake([self limitSnapshotCenterX:point.x], [self limitSnapshotCenterY:point.y]);
    //point = CGPointMake(_snapshot.center.x, [self limitSnapshotCenterY:point.y]);
    //Let the screenshot follow the gesture
    _snapshot.center = point;
    
    NSIndexPath *currentIndexPath = [self indexPathForItemAtPoint:point];
    if (!currentIndexPath) {
        return;
    }
    
    UICollectionViewCell *selectedCell = [self cellForItemAtIndexPath:_selectedIndexPath];
    selectedCell.hidden = YES;

    if (self.dataSource && [self.dataSource respondsToSelector:@selector(collectionView:canMoveItemAtIndexPath:)]) {
        if (![self.dataSource collectionView:self canMoveItemAtIndexPath:currentIndexPath]) {
            return;
        }
    }

    if (currentIndexPath && ![_selectedIndexPath isEqual:currentIndexPath]) {
        //Exchange data source and cell
        [self updateDataSourceAndCellFromIndexPath:_selectedIndexPath toIndexPath:currentIndexPath];
        if (self.delegate && [self.delegate respondsToSelector:@selector(collectionView:didMoveCellFromIndexPath:toIndexPath:)]) {
            [self.delegate collectionView:self didMoveCellFromIndexPath:_selectedIndexPath toIndexPath:currentIndexPath];
        }
        _selectedIndexPath = currentIndexPath;
    }
}
  1. 手势结束时,cell显示,snapshot从父视图移除;
- (void)gestureEndedOrCancelled:(UILongPressGestureRecognizer *)gesture {
    if (_canEdgeScroll) {
        [self stopEdgeScroll];
    }
    
    if (self.delegate && [self.delegate respondsToSelector:@selector(collectionView:endMoveCellAtIndexPath:)]) {
        [self.delegate collectionView:self endMoveCellAtIndexPath:_selectedIndexPath];
    }
    UICollectionViewCell *cell = [self cellForItemAtIndexPath:_selectedIndexPath];
    cell.alpha = 0.7;

    [UIView animateWithDuration:0.3 animations:^{
        self.snapshot.transform = CGAffineTransformIdentity;
        self.snapshot.frame = cell.frame;
//        self.snapshot.frame = CGRectMake((cell.frame.size.width - self.snapshot.frame.size.width)/2.0f, cell.frame.origin.y + (cell.frame.size.height - self.snapshot.frame.size.height)/2.0, self.snapshot.frame.size.width, self.snapshot.frame.size.height);
    } completion:^(BOOL finished) {
        cell.hidden = NO;
        [self.snapshot removeFromSuperview];
        self.snapshot = nil;
    }];
    // 延时后更新整个数据源
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self reloadData];
    });
}
  1. 考虑拖拽边界情况,内容过多可滑动时,拖拽到边缘,调整contentOffset;
#pragma mark EdgeScroll
- (void)startEdgeScroll {
    _edgeScrollLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(processEdgeScroll)];
    [_edgeScrollLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)processEdgeScroll {
    CGFloat minOffsetY = self.contentOffset.y + _edgeScrollTriggerRange;
    CGFloat maxOffsetY = self.contentOffset.y + self.bounds.size.height - _edgeScrollTriggerRange;
    CGPoint touchPoint = _snapshot.center;

    if (touchPoint.y < minOffsetY) {
        //Cell is moving up
        CGFloat moveDistance = (minOffsetY - touchPoint.y)/_edgeScrollTriggerRange*_maxScrollSpeedPerFrame;
        _currentScrollSpeedPerFrame = moveDistance;
        self.contentOffset = CGPointMake(self.contentOffset.x, [self limitContentOffsetY:self.contentOffset.y - moveDistance]);
    } else if (touchPoint.y > maxOffsetY) {
        //Cell is moving down
        CGFloat moveDistance = (touchPoint.y - maxOffsetY)/_edgeScrollTriggerRange*_maxScrollSpeedPerFrame;
        _currentScrollSpeedPerFrame = moveDistance;
        self.contentOffset = CGPointMake(self.contentOffset.x, [self limitContentOffsetY:self.contentOffset.y + moveDistance]);
    }
    [self setNeedsLayout];
    [self layoutIfNeeded];

    [self gestureChanged:_longPressGesture];
}

- (void)stopEdgeScroll {
    _currentScrollSpeedPerFrame = 0;
    if (_edgeScrollLink) {
        [_edgeScrollLink invalidate];
        _edgeScrollLink = nil;
    }
}
Demo Git地址

SDMovableCellCollectionView

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值