現(xiàn)在一些常用的App都有分類功能,為了讓用戶方便的編輯自己喜好的分類標(biāo)簽,相應(yīng)的要有分類編輯頁(yè)。以下內(nèi)容簡(jiǎn)單的介紹了如何實(shí)現(xiàn)類似騰訊視頻、今日頭條的內(nèi)容分類標(biāo)簽編輯功能。本篇文章主要講使用UICollectionView如何實(shí)現(xiàn)分類標(biāo)簽編輯功能,分為使用iOS9以后系統(tǒng)自帶的新特性實(shí)現(xiàn),使用UICollectionView自定義方式實(shí)現(xiàn)。主要涉及到的技術(shù)點(diǎn)有:UICollectionView的Cell移動(dòng)、長(zhǎng)按手勢(shì)識(shí)別、NSArray數(shù)組操作、控件封裝、數(shù)據(jù)本地持久化、文件管理等等。
下載Demo
一、思考實(shí)現(xiàn)方式
1、使用
UIButton、UIScrollView實(shí)現(xiàn):這種方式需要?jiǎng)?chuàng)建很多UIButton按鈕,不斷的更新按鈕的布局;
2、使用UICollectionView的iOS9以后系統(tǒng)自帶的新特性實(shí)現(xiàn):簡(jiǎn)單快捷,但是無(wú)法支持iOS9以下的設(shè)備;
3、使用UICollectionView實(shí)現(xiàn)布局,標(biāo)簽移動(dòng)通過(guò)開(kāi)發(fā)者自己控制實(shí)現(xiàn):兼容性好,開(kāi)發(fā)者可控性強(qiáng),需要自定義一個(gè)與選中cell內(nèi)容相同ShowView展示給用戶,ShowView的位置隨手勢(shì)的移動(dòng)改變,手勢(shì)結(jié)束后ShowView消失,此時(shí)顯示真正的cell;
二、實(shí)現(xiàn)運(yùn)行效果
三、創(chuàng)建UICollectionView
這個(gè)很簡(jiǎn)單,大家已經(jīng)使用的很熟練了,就不再過(guò)多介紹了。
/* UICollectionView的布局layout */
_flowLayout = [[UICollectionViewFlowLayout alloc] init];
CGFloat cellWidth = (CGRectGetWidth(self.view.frame) - (kColumnNumner + 1)*kItemMarginX)/kColumnNumner;
CGFloat cellHeight = cellWidth/2.0f;
_flowLayout.itemSize = CGSizeMake(cellWidth, cellHeight);
_flowLayout.sectionInset = UIEdgeInsetsMake(kItemMarginY, kItemMarginX, kItemMarginY, kItemMarginX);
_flowLayout.minimumLineSpacing = kItemMarginY;
_flowLayout.minimumInteritemSpacing = kItemMarginX;
_flowLayout.headerReferenceSize = CGSizeMake(CGRectGetWidth(self.view.frame), 60);
self.collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, CGRectGetMaxY(lineView.frame), CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame) - CGRectGetMaxY(lineView.frame)) collectionViewLayout:_flowLayout];
self.collectionView.backgroundColor = [UIColor clearColor];
self.collectionView.showsHorizontalScrollIndicator = YES;
/* 注冊(cè)cell */
[self.collectionView registerNib:[UINib nibWithNibName:@"WSClassifyEditViewCell" bundle:nil] forCellWithReuseIdentifier:@"WSClassifyEditViewCell"];
/* 注冊(cè)SectionHeader */
[self.collectionView registerClass:[WSClassifyHeaderView class]
forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"WSClassifyHeaderView"];
self.collectionView.delegate = self;
self.collectionView.dataSource = self;
[self.view addSubview:self.collectionView];
四、為UICollectionView添加長(zhǎng)按手勢(shì)
為UICollectionView添加長(zhǎng)按手勢(shì)UILongPressGestureRecognizer,addGestureRecognizer添加手勢(shì)到UICollectionView上,handleLongPressGesture為長(zhǎng)按手勢(shì)事件觸發(fā)方法,通過(guò)監(jiān)聽(tīng)長(zhǎng)按手勢(shì)的Began、Changed、Cancelled、Ended實(shí)現(xiàn)選中cell的移動(dòng)功能。
長(zhǎng)按手勢(shì)移動(dòng)cell思路:
1、長(zhǎng)按選中的需要移動(dòng)拖動(dòng)的
cell,longGesture的狀態(tài)變?yōu)?code>begin,通過(guò)手勢(shì)locationInView獲取當(dāng)前點(diǎn)擊位置point,初始化一個(gè)專門用于顯示的ShowView,ShowView的位置隨point的改變而變化,也就是說(shuō)ShowView需要跟隨手勢(shì)ShowView.center=point;
2、拖動(dòng)cell此時(shí)手勢(shì)的狀態(tài)變?yōu)?code>Changed,通過(guò)手勢(shì)``locationInView獲取的當(dāng)前位置改變,ShowView的位置隨之變化;
3、通過(guò)ShowView的位置找到和cell交換的targetCell的位置;
4、交換cell和targetCell的位置;
5、處理數(shù)據(jù)結(jié)構(gòu),這里是對(duì)兩個(gè)數(shù)組進(jìn)行操作;
添加手勢(shì):
_longGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)];
_longGesture.delegate = self;
[self.collectionView addGestureRecognizer:_longGesture];
手勢(shì)觸發(fā):
- (void)handleLongPressGesture:(UILongPressGestureRecognizer *)gestureRecognizer {
CGPoint point = [gestureRecognizer
locationInView:self.collectionView];
switch (gestureRecognizer.state) {
case UIGestureRecognizerStateBegan:
[self dragItemBegin:point];
break;
case UIGestureRecognizerStateChanged:
[self dragItemChanged:point];
break;
case UIGestureRecognizerStateCancelled:
[self dragItemCancelled];
break;
case UIGestureRecognizerStateEnded:
[self dragItemEnd];
break;
default:
break;
}
}
五、手勢(shì)事件實(shí)現(xiàn)
開(kāi)始長(zhǎng)按手勢(shì)Begin:
自定義方法實(shí)現(xiàn):
1、通過(guò)locationInView獲取點(diǎn)擊位置point;
2、根據(jù)point獲取當(dāng)前選中cell的位置NSIndexPath *currentIndexPath;
3、判斷點(diǎn)擊范圍;
4、再根據(jù)currentIndexPath獲取選中的cell實(shí)例;
5、創(chuàng)建個(gè)輔助顯示的ShowView,根據(jù)選中的cell獲取的截圖image,將image貼到ShowView中,選中cell隱藏,并對(duì)ShowView添加一個(gè)放大動(dòng)畫,讓ShowView隨著手勢(shì)移動(dòng)而不是cell在移動(dòng);
//第一步:通過(guò)locationInView獲取點(diǎn)擊位置point;
CGPoint point = [gestureRecognizer locationInView:self.collectionView];
- (void)dragItemBegin:(CGPoint)point
{
//第二步:根據(jù)point獲取當(dāng)前選中cell的位置NSIndexPath *currentIndexPath;
NSIndexPath *currentIndexPath = [self.collectionView indexPathForItemAtPoint:point];
//第三步:判斷點(diǎn)擊范圍;
if (![self collectionView:self.collectionView canMoveItemAtIndexPath:currentIndexPath]) {
return;
}
self.selectedIndexPath = currentIndexPath;
//第四步:再根據(jù)currentIndexPath獲取選中的cell實(shí)例;
WSClassifyEditViewCell *viewcell = (WSClassifyEditViewCell *)[self.collectionView cellForItemAtIndexPath:self.selectedIndexPath];
//第五步:創(chuàng)建個(gè)輔助顯示的ShowView,根據(jù)選中的cell獲取cell的截圖image,將image貼到ShowView中,選中cell隱藏,并對(duì)ShowView添加一個(gè)放大動(dòng)畫,讓ShowView隨著手勢(shì)移動(dòng)而不是cell在移動(dòng)
self.showView = [[UIView alloc] initWithFrame:viewcell.frame];
UIImageView *imageView = [[UIImageView alloc] initWithImage:[self getImageContext:viewcell]];
imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self.showView addSubview:imageView];
[self.collectionView addSubview:self.currentView];
viewcell.isMoving = YES;
[UIView animateWithDuration:0.3 delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
self.currentView.transform = CGAffineTransformMakeScale(1.1f, 1.1f);
} completion:^(BOOL finished) {
}];
}
使用
iOS9以后系統(tǒng)自帶的新特性實(shí)現(xiàn):
手勢(shì)狀態(tài)為begin時(shí)只需一句話,蠻簡(jiǎn)單!
[self.collectionView beginInteractiveMovementForItemAtIndexPath:self.selectedIndexPath];
長(zhǎng)按手勢(shì)拖動(dòng)變化Changed:
自定義方法實(shí)現(xiàn):
1、通過(guò)locationInView獲取手勢(shì)移動(dòng)位置point;
2、更新showView的位置;
3、獲取將要交換的目標(biāo)位置的IndexPath;
4、判斷showView移動(dòng)到的target位置是否需要交換位置;
5、處理內(nèi)存中的數(shù)據(jù),交換數(shù)據(jù)位置;
6、選中cell與targetCell交換位置;
7、將記錄選中位置的變量selectedIndexPath進(jìn)行更新為新位置;
//第一步:通過(guò)locationInView獲取手勢(shì)移動(dòng)位置point;
CGPoint point = [gestureRecognizer locationInView:self.collectionView];
-(void)dragItemChanged:(CGPoint)point{
//第二步:更新showView的位置
self.showView.center = point;
//第三步:獲取將要交換的目標(biāo)位置的IndexPath
NSIndexPath *newIndexPath = [self.collectionView indexPathForItemAtPoint:self.showView.center];
//當(dāng)前選中cell的位置,selectedIndexPath是通過(guò)第一步定位操作獲取的,用于保存正在被拖動(dòng)cell的indexPath
NSIndexPath *previousIndexPath = self.selectedIndexPath;
//第四步:判斷showView移動(dòng)到的target位置是否需要交換位置;
if (![self collectionView:_collectionView itemAtIndexPath:previousIndexPath canMoveToIndexPath:newIndexPath]) {
return;
}
//第五步:處理內(nèi)存中的數(shù)據(jù),交換數(shù)據(jù)位置;
id obj = [self.displayArray objectAtIndex:self.selectedIndexPath.row];
[self.displayArray removeObject:obj];
[self.displayArray insertObject:obj atIndex:newIndexPath.row];
//第六步:選中cell與targetCell交換位置;
[self.collectionView moveItemAtIndexPath:previousIndexPath toIndexPath:newIndexPath];
//第七步:將記錄選中位置的變量selectedIndexPath進(jìn)行更新為新位置;
self.selectedIndexPath = newIndexPath;
}
第四步中的判斷`showView`移動(dòng)到目標(biāo)位置是否需要交換位置的方法
- (BOOL)collectionView:(UICollectionView *)collectionView itemAtIndexPath:(NSIndexPath *)fromIndexPath canMoveToIndexPath:(NSIndexPath *)toIndexPath
{
//移動(dòng)到顯示分組的第一個(gè)位置時(shí)不需要交換位置
if (toIndexPath.section == 0) {
if (toIndexPath.row == 0) {
return NO;
}
}
else if (toIndexPath.section == 1){
//移動(dòng)到未顯示分組時(shí)不需要交換位置
return NO;
}
return YES;
}
>使用`iOS9`以后系統(tǒng)自帶的新特性實(shí)現(xiàn):
>手勢(shì)狀態(tài)為`Changed`時(shí)只需一句話,蠻簡(jiǎn)單!
CGPoint point = [gestureRecognizer locationInView:self.collectionView];
[self.collectionView updateInteractiveMovementTargetPosition:point];
長(zhǎng)按手勢(shì)拖動(dòng)變化結(jié)束`Ended`:
>使用自定義方法實(shí)現(xiàn):
>1、通過(guò)記錄當(dāng)前位置的`selectedIndexPath`獲取當(dāng)前位置的`frame`;
>2、將`showView`的`frame`置未第一步中獲取的`frame`,模擬`cell`復(fù)位;
>3、清空輔助`showView`,并顯示`cell`;
>```
-(void)dragItemEnd
{
//第一步:通過(guò)記錄當(dāng)前位置的selectedIndexPath獲取當(dāng)前位置的frame;
WSClassifyEditViewCell *viewcell = (WSClassifyEditViewCell *) [self.collectionView cellForItemAtIndexPath:self.selectedIndexPath];
CGRect endFrame = viewcell.frame;
[UIView animateWithDuration:0.3 delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
//第二步:將showView的frame置未第一步中獲取的frame,模擬cell復(fù)位
self.showView.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
self.showView.frame = endFrame;
}completion:^(BOOL finished) {
//清空輔助showView,并顯示cell;
[self.showView removeFromSuperview];
self.showView = nil;
viewcell.isMoving = NO;
[self.flowLayout invalidateLayout];
}];
}
使用
iOS9以后系統(tǒng)自帶的新特性實(shí)現(xiàn):
手勢(shì)狀態(tài)為Ended時(shí)也只需一句話,蠻簡(jiǎn)單!
[self.collectionView endInteractiveMovement];
六、點(diǎn)擊cell實(shí)現(xiàn)加減功能
在編輯狀態(tài)下點(diǎn)擊顯示分組的cell,在顯示分組中刪除,將選中的cell移動(dòng)到未顯示分組中,并且恢復(fù)到添加前的位置。點(diǎn)擊未顯示分組的cell,在未顯示分組中刪除,將被選中的cell添加到顯示分組中;
主要思路:
1、需要實(shí)現(xiàn)分類標(biāo)簽數(shù)據(jù)的本地持久化存儲(chǔ),我將全部標(biāo)簽存在沙盒目錄下的allSorts.plist中,同時(shí)將已經(jīng)顯示的分類標(biāo)簽存在沙盒目錄下的displaySort.plist中,每次顯示分類編輯頁(yè)的時(shí)候?qū)山M數(shù)據(jù)取出,對(duì)比兩組數(shù)據(jù)獲取未顯示分類標(biāo)簽數(shù)據(jù)。并且實(shí)現(xiàn)總分類標(biāo)簽因服務(wù)器數(shù)據(jù)出現(xiàn)增減標(biāo)簽時(shí)與本地?cái)?shù)據(jù)合并的功能,新分類標(biāo)簽在下次啟動(dòng)時(shí)顯示;
1、合并服務(wù)器數(shù)據(jù),解決數(shù)據(jù)增減問(wèn)題:
/// 判斷本地?cái)?shù)據(jù)與服務(wù)端數(shù)據(jù)是否相同
/// @param locals 本地?cái)?shù)據(jù)
/// @param remotes 服務(wù)器返回?cái)?shù)據(jù)
- (BOOL)equalLocal:(NSArray *)locals remote:(NSArray *)remotes
{
//查詢到在locals中,不在數(shù)組remotes中的數(shù)據(jù)
NSPredicate *localFilterPredicate = [NSPredicate predicateWithFormat:@"NOT (SELF IN %@)",remotes];
NSArray *localFilter = [locals filteredArrayUsingPredicate:localFilterPredicate];
[self.reduceObjects addObjectsFromArray:localFilter];
//查詢到在remotes中,不在數(shù)組locals中的數(shù)據(jù)
NSPredicate *remoteFilterPredicate = [NSPredicate predicateWithFormat:@"NOT (SELF IN %@)",locals];
NSArray *remoteFilter = [remotes filteredArrayUsingPredicate:remoteFilterPredicate];
[self.addObjects addObjectsFromArray:remoteFilter];
//拼接數(shù)組,所有變化的數(shù)據(jù),若array內(nèi)容為空,則數(shù)組未改變
NSMutableArray *array = [NSMutableArray arrayWithArray:localFilter];
[array addObjectsFromArray:remoteFilter];
if (array.count>0) {
return YES;
}
return NO;
}
2、通過(guò)比較全部標(biāo)簽數(shù)據(jù)與已顯示標(biāo)簽數(shù)據(jù),獲取未顯示標(biāo)簽:
/// 獲取未顯示數(shù)據(jù)
- (NSArray *)getNoDisplayArray
{
NSPredicate *filterPredicate = [NSPredicate predicateWithFormat:@"NOT (SELF IN %@)",self.displayArray];
NSArray *resultArray = [self.allSortArray filteredArrayUsingPredicate:filterPredicate];
return resultArray;
}
3、處理分類更新數(shù)據(jù)變化,點(diǎn)擊 cell進(jìn)行增刪時(shí)使用:
這個(gè)方法在數(shù)據(jù)管理類中實(shí)現(xiàn),將處理完的結(jié)果向UI層拋出回調(diào),實(shí)現(xiàn)是數(shù)據(jù)與UI的分離
/**
處理分類更新數(shù)據(jù)變化
*/
- (void)handleUpdateSortAtIndexPath:(NSIndexPath *)indexPath complete:(void(^)(NSArray *displayDatas,NSArray *noDisplayDatas,NSInteger row))complateBlock
{
NSInteger row = 0;
if (indexPath.section == 0) {
id obj = [self.displayArray objectAtIndex:indexPath.row];
[self.displayArray removeObject:obj];
self.noDisplayArray = (NSMutableArray *)[self getNoDisplayArray];
row = [self.noDisplayArray indexOfObject:obj];
}
else if (indexPath.section == 1){
id obj = [self.noDisplayArray objectAtIndex:indexPath.row];
[self.displayArray addObject:obj];
self.noDisplayArray = (NSMutableArray *)[self getNoDisplayArray];
row = self.displayArray.count - 1;
}
[self saveSortData:self.displayArray plistPath:self.displayPlistPath];
complateBlock(self.displayArray,self.noDisplayArray,row);
}
4、UI層中對(duì)cell增刪的處理
if ([self.delegate respondsToSelector:@selector(selectItemAtIndexPath:complete:)]) {
@weakify(self)
[self.delegate selectItemAtIndexPath:indexPath complete:^(NSArray * _Nonnull displayDatas, NSArray * _Nonnull noDisplayDatas, NSInteger row) {
@strongify(self)
self.displayArray = (NSMutableArray *)displayDatas;
self.noDisplayArray = (NSMutableArray *)noDisplayDatas;
NSIndexPath * targetIndexPath = [NSIndexPath indexPathForRow:row inSection:1];
[self handleMoveItemIndex:indexPath targetIndex:targetIndexPath];
}];
}
七、相關(guān)代理回調(diào)
1、基礎(chǔ)回調(diào)方法:
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 2;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
if (section == 0) {
return self.displayArray.count;
}
else if (section == 1){
return self.noDisplayArray.count;
}
return 0;
}
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
{
if (kind == UICollectionElementKindSectionHeader) {
WSClassifyHeaderView *header = (WSClassifyHeaderView *)[collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"WSClassifyHeaderView" forIndexPath:indexPath];
[header buildHeaderView:indexPath.section];
return header;
}
return [[UICollectionReusableView alloc] init];
}
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
static NSString* cellId = @"WSClassifyEditViewCell";
WSClassifyEditViewCell *itemCell = (WSClassifyEditViewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:cellId forIndexPath:indexPath];
[itemCell sizeToFit];
if (indexPath.section == 0) {
[itemCell bindViewModel:self.displayArray[indexPath.row] indexPath:indexPath];
}
else if (indexPath.section == 1){
[itemCell bindViewModel:self.noDisplayArray[indexPath.row] indexPath:indexPath];
}
itemCell.markHiden = self.editBtn.hidden;
return itemCell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
//在這里實(shí)現(xiàn)點(diǎn)擊事件
}
2、只有使用iOS9以后系統(tǒng)自帶的新特性實(shí)現(xiàn)時(shí)使用:
- (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath*)destinationIndexPath
{
/**
* sourceIndexPath 原始數(shù)據(jù) indexpath
* destinationIndexPath 移動(dòng)到目標(biāo)數(shù)據(jù)的 indexPath
*/
NSInteger soureceIndex = sourceIndexPath.row;
NSInteger destinaIndex = destinationIndexPath.row;
NSString *item = [self.displayArray objectAtIndex:soureceIndex];
[self.displayArray removeObjectAtIndex:soureceIndex];
[self.displayArray insertObject:item atIndex:destinaIndex];
}
- (NSIndexPath *)collectionView:(UICollectionView *)collectionView targetIndexPathForMoveFromItemAtIndexPath:(NSIndexPath *)originalIndexPath toProposedIndexPath:(NSIndexPath *)proposedIndexPath
{
//此地處理cell滑動(dòng)到Top分組第一個(gè)位置不移動(dòng),滑動(dòng)到bottom分組任何位置不移動(dòng)
if (proposedIndexPath.section == 0) {
if (proposedIndexPath.item == 0) {
return originalIndexPath;
}
}
else if (proposedIndexPath.section == 1){
return originalIndexPath;
}
return proposedIndexPath;
}
- (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == 0 && indexPath.row == 0) {
return NO;
}
else if(indexPath.section == 1){
return NO;
}
return YES;
}
注:在通過(guò)全部標(biāo)簽數(shù)據(jù)和已顯示標(biāo)簽數(shù)據(jù)比較獲取未顯示分類標(biāo)簽數(shù)據(jù)時(shí)使用了NSPredicate謂詞查詢,這里碰到了一個(gè)小問(wèn)題,在UI層處理時(shí)數(shù)據(jù)都已經(jīng)被處理為model實(shí)例化模型,導(dǎo)致了使用NSPredicate時(shí)沒(méi)有過(guò)濾出想要的數(shù)據(jù),所以我將數(shù)據(jù)處理放到了管理類中,在管理類中的數(shù)據(jù)還是原始模型(字典類型),這樣再使用NSPredicate過(guò)濾時(shí)就沒(méi)有出現(xiàn),數(shù)據(jù)內(nèi)容相同但地址不同時(shí)無(wú)法過(guò)濾的問(wèn)題了; 建議使用自定義cell移動(dòng)的方法實(shí)現(xiàn)分類標(biāo)簽編輯頁(yè),這樣比使用iOS9以后系統(tǒng)提供的結(jié)構(gòu)能更好的控制效果,并且兼容性更好。