前言
最近參與了事務流程工具化組件的開發,其中有一個模塊需要通過長按移動Table View Cells
,來達到調整任務的需求,在此記錄下開發過程中的實現思路。完成后的效果如下圖所示:
實現思路
- 添加手勢
首先給collection view
添加一個UILongGestureRecognizer
,在項目中一般使用懶加載的方式來對對象進行初始化:
- (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;
}
}
- 長按手勢狀態為開始
主要處理兩個方面的事務,一為獲取當前長按手勢所對應的Table View Cell
的鏡像,將其添加到Collection View
上。二為一些初始狀態的設置,后續在移動后網絡請求出錯及判斷當前手勢所處的Table View
和上一次是否一致需要使用到。最后調用startPageEdgeScroll
開啟定時器。
- (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
上,開啟定時器,來處理長按手勢狀態為改變時的情況。
- (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 View
和 Collection 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];
}
- 長按手勢狀態為取消或結束
取消計時器,設置Collection View
的偏移量,讓其Collection View Cell
位于屏幕的中心,發送網絡請求,去調整任務的排序,同時將鏡像視圖隱藏,并將其所對應的Table View Cell
顯示出來。
- (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
對象中。
@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.x
和iOS9.x
時,會出現以后要做
界面不會回彈的情況。如下圖所示:
經排查,是在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時,新建計劃時,新建窗口會上移一段,如下圖所示:
分析發現,應該是監聽鍵盤高度變化時,輸入框的高度計算在特定機型的特定版本上計算錯誤,將原有的計算
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];
}
切換輸入法時,輸入框被鍵盤遮住問題
在修復此問題后,自測時發現,輸入法由簡體拼音切換為表情符號時,輸入框會被鍵盤擋住,在代碼中打斷點發現UIKeyboardWillShowNotification
和UIKeyboardWillChangeFrameNotification
通知均未被觸發,同時對比微信發現,切換輸入法時,同時開啟了自動校正功能,所以參考添加如下代碼:
_textView.internalTextView.autocorrectionType = UITextAutocorrectionTypeYes;
解決切換輸入法時,輸入框被鍵盤遮住的問題。
總結
除了上述Table View Cell
移動的操作,在項目中還處理了創建事務和事務詳情相關的業務。在整個過程中,比較棘手的還是Table View Cell
的移動,在開發過程中,有時數據的移動和Table View Cell
的移動未對應上,造成Table View Cell
布局錯亂,排查了很久。在項目開發過程中,還是需要仔細去分析問題,然后再去尋求方法去解決問題。
文章所對應的Demo
請點這里
本文已經同步到我的個人技術博客: 傳送門 ,歡迎常來^^。
參考的文章鏈接如下
利用長按手勢移動 Table View Cells