iOS学习专题iOS接下来要研究的知识点

iOS长按移动Table View Cells

2018-01-27  本文已影响165人  LeverTsui

前言

最近参与了事务流程工具化组件的开发,其中有一个模块需要通过长按移动Table View Cells,来达到调整任务的需求,在此记录下开发过程中的实现思路。完成后的效果如下图所示:

长按移动cell.gif

实现思路

- (UICollectionView *)collectionView {
    if (!_collectionView) {
        _collectionView =  [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:self.flowLayout];
        
        _collectionView.backgroundColor = [UIColor whiteColor];
        _collectionView.dataSource = self;
        _collectionView.delegate = self;
        
        [_collectionView registerClass:[TLCMainCollectionViewCell class] forCellWithReuseIdentifier:[TLCMainCollectionViewCell identifier]];
        
        _collectionView.showsHorizontalScrollIndicator = NO;
        _collectionView.showsVerticalScrollIndicator = NO;
        _collectionView.bounces = YES;
        _collectionView.decelerationRate = 0;
        
        [_collectionView addGestureRecognizer:self.longPress];
    }
    return _collectionView;
}
- (UILongPressGestureRecognizer *)longPress {
    if (!_longPress) {
        _longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressGestureRecognized:)];
    }
    return _longPress;
}

在用户长按后,触犯长按事件,先获取到当前手势所在的collection view位置,再做后续的处理。

- (void)longPressGestureRecognized:(UILongPressGestureRecognizer *)sender {
    
    CGPoint location = [sender locationInView:sender.view];
    
    UIGestureRecognizerState state = sender.state;
    switch (state) {
            case UIGestureRecognizerStateBegan: {
                [self handleLongPressStateBeganWithLocation:location];
            }
            break;
            
            case UIGestureRecognizerStateChanged: {
            }
            break;
            
            case UIGestureRecognizerStateEnded:
            case UIGestureRecognizerStateCancelled: {
                [self longGestureEndedOrCancelledWithLocation:location];
            }
            break;
            
        default:
            break;
    }
}
- (void)handleLongPressStateBeganWithLocation:(CGPoint)location {
    
    TLCMainCollectionViewCell *selectedCollectionViewCell = [self currentTouchedCollectionCellWithLocation:location];
    
    NSIndexPath *touchIndexPath = [self longGestureBeganIndexPathForRowAtPoint:location atTableView:selectedCollectionViewCell.tableView];
   
    if (!selectedCollectionViewCell || !touchIndexPath) {
        return ;
    }
    self.selectedCollectionViewCellRow = [self.collectionView indexPathForCell:selectedCollectionViewCell].row;
    
    // 已完成的任务,不支持排序
    TLPlanItem *selectedItem = [self.viewModel itemAtIndex:self.selectedCollectionViewCellRow
                                              subItemIndex:touchIndexPath.section];
    if (!selectedItem || selectedItem.finish) {
        return;
    }
    selectedItem.isHidden = YES;
    
    self.snapshotView = [self snapshotViewWithTableView:selectedCollectionViewCell.tableView
                                            atIndexPath:touchIndexPath];
    [self.collectionView addSubview:self.snapshotView];
    
    self.selectedIndexPath = touchIndexPath;
    self.originalSelectedIndexPathSection = touchIndexPath.section;
    self.originalCollectionViewCellRow = self.selectedCollectionViewCellRow;
    self.previousPoint = CGPointZero;
    
    [self startPageEdgeScroll];
}
- (void)startPageEdgeScroll {
    self.edgeScrollTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(pageEdgeScrollEvent)];
    [self.edgeScrollTimer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}

在定时器触发的事件中,处理两个方面的事情,移动cell和滚动ScrollView

- (void)pageEdgeScrollEvent {
    [self longGestureChanged:self.longPress];
    
    CGFloat snapshotViewCenterOffsetX =  [self touchSnapshotViewCenterOffsetX];
    
    if (fabs(snapshotViewCenterOffsetX) > (TLCMainViewControllerFlowLayoutWidthOffset-20)) {
        //横向滚动
        [self handleScrollViewHorizontalScroll:self.collectionView viewCenterOffsetX:snapshotViewCenterOffsetX];
    } else {
        //垂直滚动
        [self handleScrollViewVerticalScroll:[self selectedCollectionViewCellTableView]];
    }
}

在长按手势触摸点位置改变时,处理对应cell的移除和插入动作。横向滚动和垂直滚动主要是根据不同情况设置对应的Table ViewCollection View的内容偏移量。可以在文末的链接中查看源码。

- (void)longGestureChanged:(UILongPressGestureRecognizer *)sender {
    
    CGPoint currentPoint = [sender locationInView:sender.view];
    TLCMainCollectionViewCell *currentCollectionViewCell = [self currentTouchedCollectionCellWithLocation:currentPoint];
    if (!currentCollectionViewCell) {
        currentCollectionViewCell = [self collectionViewCellAtRow:self.selectedCollectionViewCellRow];
    }
    
    TLCMainCollectionViewCell *lasetSelectedCollectionViewCell = [self collectionViewCellAtRow:self.selectedCollectionViewCellRow];
    
    //判断targetTableView是否改变
    BOOL isTargetTableViewChanged = NO;
    if (self.selectedCollectionViewCellRow != currentCollectionViewCell.indexPath.row) {
        isTargetTableViewChanged = YES;
        self.selectedCollectionViewCellRow = currentCollectionViewCell.indexPath.row;
    }
    //获取到需要移动到的目标indexpath
    NSIndexPath *targetIndexPath = [self longGestureChangeIndexPathForRowAtPoint:currentPoint
                                                        collectionViewCell:currentCollectionViewCell];
    
    NSIndexPath *lastSelectedIndexPath = self.selectedIndexPath;
    
    TLCMainCollectionViewCell *selectedCollectionViewCell = [self collectionViewCellAtRow:self.selectedCollectionViewCellRow];
    //判断跟上一次长按手势所处的Table View是否相同,如果相同,移动cell,
    //如果不同,删除上一次所定义的cell,插入到当前位置
    if (isTargetTableViewChanged) {
        if ([[self selectedCollectionViewCellTableView] numberOfSections]>targetIndexPath.section) {
            [[self selectedCollectionViewCellTableView] scrollToRowAtIndexPath:targetIndexPath
                                                              atScrollPosition:UITableViewScrollPositionNone animated:YES];
        }
        
        TLPlanItem *moveItem = [self.viewModel itemAtIndex:lasetSelectedCollectionViewCell.indexPath.row
                                              subItemIndex:lastSelectedIndexPath.section];
        [self.viewModel removeObject:moveItem
                           itemIndex:lasetSelectedCollectionViewCell.indexPath.row];
        [self.viewModel insertItem:moveItem
                             index:self.selectedCollectionViewCellRow
                      subItemIndex:targetIndexPath.section];

        [lasetSelectedCollectionViewCell updateCellWithData:[self planItemsAtIndex:lasetSelectedCollectionViewCell.indexPath.row]];
        [lasetSelectedCollectionViewCell.tableView deleteSections:[NSIndexSet indexSetWithIndex:lastSelectedIndexPath.section]
                                                 withRowAnimation:UITableViewRowAnimationNone];

        [selectedCollectionViewCell updateCellWithData:[self planItemsAtIndex:self.selectedCollectionViewCellRow]];
        [selectedCollectionViewCell.tableView insertSections:[NSIndexSet indexSetWithIndex:targetIndexPath.section]
                                            withRowAnimation:UITableViewRowAnimationNone];
    } else {
        BOOL isSameSection = lastSelectedIndexPath.section == targetIndexPath.section;
        UITableViewCell *targetCell = [self tableView:[self selectedCollectionViewCellTableView]
                                selectedCellAtSection:targetIndexPath.section];
        if (isSameSection || !targetCell ) {
            [self modifySnapshotViewFrameWithTouchPoint:currentPoint];
            return;
        }
        
        TLPlanItem *item = [self.viewModel itemAtIndex:self.selectedCollectionViewCellRow
                                          subItemIndex:lastSelectedIndexPath.section];
        [self.viewModel removeObject:item
                           itemIndex:self.selectedCollectionViewCellRow];
        [self.viewModel insertItem:item
                             index:self.selectedCollectionViewCellRow
                      subItemIndex:targetIndexPath.section];
        
        [selectedCollectionViewCell updateCellWithData:[self planItemsAtIndex:self.selectedCollectionViewCellRow]];
        [selectedCollectionViewCell.tableView moveSection:lastSelectedIndexPath.section
                                                toSection:targetIndexPath.section];
    }
    
    self.selectedIndexPath = targetIndexPath;
    //改变长按cell镜像的位置
    [self modifySnapshotViewFrameWithTouchPoint:currentPoint];
}
- (void)longGestureEndedOrCancelledWithLocation:(CGPoint)location {
    
    [self stopEdgeScrollTimer];
    
    CGPoint contentOffset = [self.flowLayout targetContentOffsetForProposedContentOffset:self.collectionView.contentOffset
                                                                   withScrollingVelocity:CGPointZero];
    [self.collectionView setContentOffset:contentOffset animated:YES];
    
    UITableViewCell *targetCell = [[self selectedCollectionViewCellTableView] cellForRowAtIndexPath:self.selectedIndexPath];
    
    if ([self canAdjustPlanRanking]) {
        [self adjustPlanRanking];
    }
    TLPlanItem *slectedItem = [self.viewModel itemAtIndex:self.selectedCollectionViewCellRow subItemIndex:self.selectedIndexPath.section];
    [UIView animateWithDuration:0.25 animations:^{
        self.snapshotView.transform = CGAffineTransformIdentity;
        self.snapshotView.frame = [self snapshotViewFrameWithCell:targetCell];
        
    } completion:^(BOOL finished) {
        targetCell.hidden = NO;
        slectedItem.isHidden = NO;
        [self.snapshotView removeFromSuperview];
        self.snapshotView = nil;
    }];
}
@interface TLCMainViewModel : NSObject 

/**
 今日要做、下一步要做和以后要做
 */
@property (nonatomic, readonly, strong) NSArray <NSString *> *titleArray; 

/**
 获取计划列表
 
 @param completion  TLTodoModel
 */
- (void)obtainTotalPlanListWithTypeCompletion:(TLSDKCompletionBlk)completion;

/**
 添加计划

 @param requestItem requestItem
 @param completion 完成回调
 */
- (void)addPlanWithReq:(TLPlanItemReq *)requestItem
           atIndexPath:(NSIndexPath *)indexPath
            completion:(TLSDKCompletionBlk)completion;

/**
 返回显示的collectionViewCell的个数

 @return 数据的个数
 */
- (NSInteger)numberOfItems;

/**
 根据type获取对应的数据

 @param index 位置
 @return 此计划所对应的数据
 */
- (NSMutableArray<TLPlanItem *> *)planItemsAtIndex:(NSInteger)index;

/**
 删除某个计划
 
 @param itemIndex 单项数据在数组中的位置,如今日计划中的数据,itemIndex为0
 @param subItemIndex  单项数据数组中所在的位置
 @param completion 完成回调
 */
- (void)deletePlanAtItemIndex:(NSInteger)itemIndex
                 subItemIndex:(NSInteger)subItemIndex
                   completion:(dispatch_block_t)completion;


/**
 修改计划状态:完成与非完成
 
 @param itemIndex 单项数据在数组中的位置,如今日计划中的数据,itemIndex为0
 @param subItemIndex  单项数据数组中所在的位置
 @param completion 完成回调
 */
- (void)modiflyPlanStateAtItemIndex:(NSInteger)itemIndex
                       subItemIndex:(NSInteger)subItemIndex
                         completion:(TLSDKCompletionBlk)completion;


/**
 修改计划的title和重点标记状态

 @param itemIndex 单项数据在数组中的位置,如今日计划中的数据,itemIndex为0
 @param subItemIndex 单项数据数组中所在的位置
 @param targetItem 目标对象
 @param completion 完成回调
 */
- (void)modiflyItemAtIndex:(NSInteger)itemIndex
              subItemIndex:(NSInteger)subItemIndex
                targetItem:(TLPlanItem *)targetItem
                completion:(dispatch_block_t)completion;


/**
 移除数据

 @param item item
 @param itemIndex 单项数据在数组中的位置
 */
- (void)removeObject:(TLPlanItem *)item
           itemIndex:(NSInteger)itemIndex;

/**
 插入数据
 
 @param item 插入的对象模型
 @param itemIndex 单项数据在数组中的位置,如今日计划中的数据,itemIndex为0
 @param subItemIndex 单项数据数组中所在的位置
 */
- (void)insertItem:(TLPlanItem *)item
             index:(NSInteger)itemIndex
      subItemIndex:(NSInteger)subItemIndex;

/**
 获取数据

 @param itemIndex 一级index
 @param subItemIndex 二级index
 @return 数据模型
 */
- (TLPlanItem *)itemAtIndex:(NSInteger)itemIndex
               subItemIndex:(NSInteger)subItemIndex; 

/**
 重置数据
 */
- (void)reset;

/**
 保存长按开始时的数据
 */
- (void)storePressBeginState;

@end

代码完善

cell未居中显示问题

2018年2月1号
在iPhone系统版本为iOS8.xiOS9.x时,会出现以后要做界面不会回弹的情况。如下图所示:

bug1.png

经排查,是在UICollectionViewFlowLayout类中的

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity

方法计算得出的proposedContentOffset有偏差,修改后如下所示:

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
    CGFloat rawPageValue = self.collectionView.contentOffset.x / [self tlc_pageWidth];
    CGFloat currentPage = (velocity.x > 0.0) ? floor(rawPageValue) : ceil(rawPageValue);
    CGFloat nextPage = (velocity.x > 0.0) ? ceil(rawPageValue) : floor(rawPageValue);
    
    BOOL pannedLessThanAPage = fabs(1 + currentPage - rawPageValue) > 0.5;
    BOOL flicked = fabs(velocity.x) > [self tlc_flickVelocity];
    CGFloat actualPage = 0.0;
    
    if (pannedLessThanAPage && flicked) {
        proposedContentOffset.x = nextPage * [self tlc_pageWidth];
        actualPage = nextPage;
    } else {
        proposedContentOffset.x = round(rawPageValue) * [self tlc_pageWidth];
        actualPage = round(rawPageValue);
    } 
    if (lround(actualPage) >= 1) {
        proposedContentOffset.x -= 4.5;
    } 
    //下面为添加的代码
    if (lround(actualPage) >= 2) {
        proposedContentOffset.x = self.collectionView.contentSize.width - TLCScreenWidth;
    }
    
    return proposedContentOffset;
}
在系统版本为iOS9.x时,输入框会上一段距离问题

2018年2月12号
在机型为iPhone SE,系统版本为iOS9.x时,新建计划时,新建窗口会上移一段,如下图所示:

适配问题.png
分析发现,应该是监听键盘高度变化时,输入框的高度计算在特定机型的特定版本上计算错误,将原有的计算frame的来布局的方式改为自动布局。监听键盘高度改变的代码修改后如下:

- (void)viewWillAppear:(BOOL)animated{
    
    [super viewWillAppear:animated]; 
    [self addObserverForKeybord];
}

- (void)viewWillDisappear:(BOOL)animated {
    
    [super viewWillDisappear:animated];
    [self.view endEditing:YES];
    [self removeobserverForKeybord];
}
#pragma mark - keyboard observer

- (void)addObserverForKeybord {
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(keyboardWillShow:)
                                                 name:UIKeyboardWillShowNotification
                                               object:nil];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(keyboardWillHide:)
                                                 name:UIKeyboardWillHideNotification
                                               object:nil];
}

- (void)removeobserverForKeybord {
    
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:UIKeyboardWillShowNotification
                                                  object:nil];
    
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:UIKeyboardWillHideNotification
                                                  object:nil];
}

- (void)keyboardWillShow:(NSNotification *)notification {
    
    CGRect keyboardBounds;
    [[notification.userInfo valueForKey:UIKeyboardFrameEndUserInfoKey] getValue:&keyboardBounds];
    NSNumber *duration = [notification.userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey];
    NSNumber *curve = [notification.userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey];
    
    keyboardBounds = [self.view convertRect:keyboardBounds toView:nil];
    
    [self.inputProjectView mas_updateConstraints:^(MASConstraintMaker *make) {
        make.bottom.equalTo(self.view).offset(-CGRectGetHeight(keyboardBounds));
    }];
    
    //设置动画
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationBeginsFromCurrentState:YES];
    [UIView setAnimationDuration:[duration doubleValue]];
    [UIView setAnimationCurve:[curve intValue]];
    
    [self layoutIfNeeded];
    
    [UIView commitAnimations];
}

- (void)keyboardWillHide:(NSNotification *)notification {
    
    if([self.inputProjectView inputText].length > 0) {
        [self.inputProjectView resetText];
    }
    
    CGRect keyboardBounds;
    [[notification.userInfo valueForKey:UIKeyboardFrameEndUserInfoKey] getValue:&keyboardBounds];
    NSNumber *duration = [notification.userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey];
    NSNumber *curve = [notification.userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey];
    
    keyboardBounds = [self.view convertRect:keyboardBounds toView:nil];
    
    [self.inputProjectView mas_updateConstraints:^(MASConstraintMaker *make) {
        if (@available(iOS 11.0, *)) {
            make.bottom.equalTo(self.view).offset(self.view.safeAreaInsets.bottom+88);
        } else {
            make.bottom.equalTo(self.view).offset(88);
        }
        make.height.mas_equalTo(88);
    }];
    
    //设置动画
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationBeginsFromCurrentState:YES];
    [UIView setAnimationDuration:[duration doubleValue]];
    [UIView setAnimationCurve:[curve intValue]];
    
    [self layoutIfNeeded];
    
    [UIView commitAnimations];
}
切换输入法时,输入框被键盘遮住问题

在修复此问题后,自测时发现,输入法由简体拼音切换为表情符号时,输入框会被键盘挡住,在代码中打断点发现UIKeyboardWillShowNotificationUIKeyboardWillChangeFrameNotification通知均未被触发,同时对比微信发现,切换输入法时,同时开启了自动校正功能,所以参考添加如下代码:

 _textView.internalTextView.autocorrectionType = UITextAutocorrectionTypeYes;

解决切换输入法时,输入框被键盘遮住的问题。

总结

除了上述Table View Cell移动的操作,在项目中还处理了创建事务和事务详情相关的业务。在整个过程中,比较棘手的还是Table View Cell的移动,在开发过程中,有时数据的移动和Table View Cell的移动未对应上,造成Table View Cell布局错乱,排查了很久。在项目开发过程中,还是需要仔细去分析问题,然后再去寻求方法去解决问题。

文章所对应的Demo请点这里
本文已经同步到我的个人技术博客: 传送门 ,欢迎常来^^。
参考的文章链接如下
利用长按手势移动 Table View Cells

上一篇下一篇

猜你喜欢

热点阅读