前言
最近参与了事务流程工具化组件的开发,其中有一个模块需要通过长按移动Table View Cells
,来达到调整任务的需求,再次记录下开发过程中的实现思路。完成后的效果如下图所示:
实现思路
添加手势
首先给 collection view
添加一个 UILongGestureRecognizer
,在项目中一般使用懒加载的方式来对对象进行初始化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| - (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; }
|
1 2 3 4 5 6
| - (UILongPressGestureRecognizer *)longPress { if (!_longPress) { _longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressGestureRecognized:)]; } return _longPress; }
|
在用户长按后,触犯长按事件,先获取到当前手势所在的collection view
位置,再做后续的处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| - (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; } }
|
长按手势状态为开始
主要处理两个方面的事务,一为获取当前长按手势所对应的Table View Cell
的镜像,将其添加到 Collection View
上。二为一些初始状态的设置,后续在移动后网络请求出错及判断当前手势所处的Table View
和上一次是否一致需要使用到。最后调用startPageEdgeScroll
开启定时器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| - (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]; }
|
长按手势状态为改变
在longPressGestureRecognized
方法中,可以发现,长按手势状态改变时,并未做任何的操作,主要原因是如果在此做Table View Cells
的移动操作,如果数据超过一屏幕,无法自动将未在屏幕上的数据滚动显示出来。所以在长按手势状态为开始时,如果触摸点在Table View Cell
上,开启定时器,来处理长按手势状态为改变时的情况。
1 2 3 4
| - (void)startPageEdgeScroll { self.edgeScrollTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(pageEdgeScrollEvent)]; [self.edgeScrollTimer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; }
|
在定时器触发的事件中,处理两个方面的事情,移动cell和滚动ScrollView
。
1 2 3 4 5 6 7 8 9 10 11 12 13
| - (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 View
和 Collection View
的内容偏移量。可以在文末的链接中查看源码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| - (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]; BOOL isTargetTableViewChanged = NO; if (self.selectedCollectionViewCellRow != currentCollectionViewCell.indexPath.row) { isTargetTableViewChanged = YES; self.selectedCollectionViewCellRow = currentCollectionViewCell.indexPath.row; } NSIndexPath *targetIndexPath = [self longGestureChangeIndexPathForRowAtPoint:currentPoint collectionViewCell:currentCollectionViewCell]; NSIndexPath *lastSelectedIndexPath = self.selectedIndexPath; TLCMainCollectionViewCell *selectedCollectionViewCell = [self collectionViewCellAtRow:self.selectedCollectionViewCellRow]; 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; [self modifySnapshotViewFrameWithTouchPoint:currentPoint]; }
|
长按手势状态为取消或结束
取消计时器,设置Collection View
的偏移量,让其Collection View Cell
位于屏幕的中心,发送网络请求,去调整任务的排序,同时将镜像视图隐藏,并将其所对应的Table View Cell
显示出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| - (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; }]; }
|
数据的处理
在移动和插入Table View Cell
时,需要将其所对应的数据做响应的改变,数据相关的操作均放在TLCMainViewModel
对象中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
| @interface TLCMainViewModel : NSObject
@property (nonatomic, readonly, strong) NSArray <NSString *> *titleArray;
- (void)obtainTotalPlanListWithTypeCompletion:(TLSDKCompletionBlk)completion;
- (void)addPlanWithReq:(TLPlanItemReq *)requestItem atIndexPath:(NSIndexPath *)indexPath completion:(TLSDKCompletionBlk)completion;
- (NSInteger)numberOfItems;
- (NSMutableArray<TLPlanItem *> *)planItemsAtIndex:(NSInteger)index;
- (void)deletePlanAtItemIndex:(NSInteger)itemIndex subItemIndex:(NSInteger)subItemIndex completion:(dispatch_block_t)completion;
- (void)modiflyPlanStateAtItemIndex:(NSInteger)itemIndex subItemIndex:(NSInteger)subItemIndex completion:(TLSDKCompletionBlk)completion;
- (void)modiflyItemAtIndex:(NSInteger)itemIndex subItemIndex:(NSInteger)subItemIndex targetItem:(TLPlanItem *)targetItem completion:(dispatch_block_t)completion;
- (void)removeObject:(TLPlanItem *)item itemIndex:(NSInteger)itemIndex;
- (void)insertItem:(TLPlanItem *)item index:(NSInteger)itemIndex subItemIndex:(NSInteger)subItemIndex;
- (TLPlanItem *)itemAtIndex:(NSInteger)itemIndex subItemIndex:(NSInteger)subItemIndex;
- (void)reset;
- (void)storePressBeginState;
@end
|
代码完善
cell
未居中显示问题
2018年2月1号
在iPhone系统版本为iOS8.x
和iOS9.x
时,会出现以后要做
界面不会回弹的情况。如下图所示:
经排查,是在UICollectionViewFlowLayout
类中的
1
| - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
|
方法计算得出的proposedContentOffset
有偏差,修改后如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| - (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时,新建计划时,新建窗口会上移一段,如下图所示:
分析发现,应该是监听键盘高度变化时,输入框的高度计算在特定机型的特定版本上计算错误,将原有的计算frame
的来布局的方式改为自动布局。监听键盘高度改变的代码修改后如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
| - (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.inputProjectView 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.inputProjectView layoutIfNeeded]; [UIView commitAnimations]; }
|
切换输入法时,输入框被键盘遮住问题
在修复此问题后,自测时发现,输入法由简体拼音切换为表情符号时,输入框会被键盘挡住,在代码中打断点发现UIKeyboardWillShowNotification
和UIKeyboardWillChangeFrameNotification
通知均未被触发,同时对比微信发现,切换输入法时,同时开启了自动校正功能,所以参考添加如下代码:
1
| _textView.internalTextView.autocorrectionType = UITextAutocorrectionTypeYes;
|
解决切换输入法时,输入框被键盘遮住的问题。
总结
除了上述Table View Cell
移动的操作,在项目中还处理了创建事务和事务详情相关的业务。在整个过程中,比较棘手的还是Table View Cell
的移动,在开发过程中,有时数据的移动和Table View Cell
的移动未对应上,造成Table View Cell
布局错乱,排查了很久。在项目开发过程中,还是需要仔细去分析需求。
文章所对应的Demo
请点这里