iOS 仿腾讯视频分类编辑页面实现
现在一些常用的App都有分类功能,为了让用户方便的编辑自己喜好的分类标签,相应的要有分类编辑页。以下内容简单的介绍了如何实现类似腾讯视频、今日头条的内容分类标签编辑功能。本篇文章主要讲使用UICollectionView如何实现分类标签编辑功能,分为使用iOS9以后系统自带的新特性实现,使用UICollectionView自定义方式实现。主要涉及到的技术点有:UICollectionView的Cell移动、长按手势识别、NSArray数组操作、控件封装、数据本地持久化、文件管理等等
。
下载Demo
一、思考实现方式
1、使用
UIButton
、UIScrollView
实现:这种方式需要创建很多UIButton
按钮,不断的更新按钮的布局;
2、使用UICollectionView
的iOS9以后系统自带的新特性实现:简单快捷,但是无法支持iOS9以下的设备;
3、使用UICollectionView
实现布局,标签移动通过开发者自己控制实现:兼容性好,开发者可控性强,需要自定义一个与选中cell内容相同ShowView
展示给用户,ShowView
的位置随手势的移动改变,手势结束后ShowView
消失,此时显示真正的cell;
二、实现运行效果
三、创建UICollectionView
这个很简单,大家已经使用的很熟练了,就不再过多介绍了。
/* 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;
/* 注册cell */
[self.collectionView registerNib:[UINib nibWithNibName:@"WSClassifyEditViewCell" bundle:nil] forCellWithReuseIdentifier:@"WSClassifyEditViewCell"];
/* 注册SectionHeader */
[self.collectionView registerClass:[WSClassifyHeaderView class]
forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"WSClassifyHeaderView"];
self.collectionView.delegate = self;
self.collectionView.dataSource = self;
[self.view addSubview:self.collectionView];
四、为UICollectionView
添加长按手势
为UICollectionView
添加长按手势UILongPressGestureRecognizer
,addGestureRecognizer
添加手势到UICollectionView
上,handleLongPressGesture
为长按手势事件触发方法,通过监听长按手势的Began、Changed、Cancelled、Ended
实现选中cell
的移动功能。
长按手势移动cell
思路:
1、长按选中的需要移动拖动的
cell
,longGesture
的状态变为begin
,通过手势locationInView
获取当前点击位置point
,初始化一个专门用于显示的ShowView
,ShowView
的位置随point
的改变而变化,也就是说ShowView
需要跟随手势ShowView.center=point
;
2、拖动cell
此时手势的状态变为Changed
,通过手势``locationInView获取的当前位置改变,ShowView
的位置随之变化;
3、通过ShowView
的位置找到和cell
交换的targetCell
的位置;
4、交换cell
和targetCell
的位置;
5、处理数据结构,这里是对两个数组进行操作;
添加手势:
_longGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)];
_longGesture.delegate = self;
[self.collectionView addGestureRecognizer:_longGesture];
手势触发:
- (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;
}
}
五、手势事件实现
开始长按手势Begin
:
自定义方法实现:
1、通过locationInView
获取点击位置point
;
2、根据point
获取当前选中cell
的位置NSIndexPath *currentIndexPath
;
3、判断点击范围;
4、再根据currentIndexPath
获取选中的cell
实例;
5、创建个辅助显示的ShowView
,根据选中的cell
获取的截图image
,将image
贴到ShowView
中,选中cell
隐藏,并对ShowView
添加一个放大动画,让ShowView
随着手势移动而不是cell
在移动;
//第一步:通过locationInView获取点击位置point;
CGPoint point = [gestureRecognizer locationInView:self.collectionView];
- (void)dragItemBegin:(CGPoint)point
{
//第二步:根据point获取当前选中cell的位置NSIndexPath *currentIndexPath;
NSIndexPath *currentIndexPath = [self.collectionView indexPathForItemAtPoint:point];
//第三步:判断点击范围;
if (![self collectionView:self.collectionView canMoveItemAtIndexPath:currentIndexPath]) {
return;
}
self.selectedIndexPath = currentIndexPath;
//第四步:再根据currentIndexPath获取选中的cell实例;
WSClassifyEditViewCell *viewcell = (WSClassifyEditViewCell *)[self.collectionView cellForItemAtIndexPath:self.selectedIndexPath];
//第五步:创建个辅助显示的ShowView,根据选中的cell获取cell的截图image,将image贴到ShowView中,选中cell隐藏,并对ShowView添加一个放大动画,让ShowView随着手势移动而不是cell在移动
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
以后系统自带的新特性实现:
手势状态为begin
时只需一句话,蛮简单!
[self.collectionView beginInteractiveMovementForItemAtIndexPath:self.selectedIndexPath];
长按手势拖动变化Changed
:
自定义方法实现:
1、通过locationInView
获取手势移动位置point
;
2、更新showView
的位置;
3、获取将要交换的目标位置的IndexPath
;
4、判断showView
移动到的targe
t位置是否需要交换位置;
5、处理内存中的数据,交换数据位置;
6、选中cell与targetCell
交换位置;
7、将记录选中位置的变量selectedIndexPath
进行更新为新位置;
//第一步:通过locationInView获取手势移动位置point;
CGPoint point = [gestureRecognizer locationInView:self.collectionView];
-(void)dragItemChanged:(CGPoint)point{
//第二步:更新showView的位置
self.showView.center = point;
//第三步:获取将要交换的目标位置的IndexPath
NSIndexPath *newIndexPath = [self.collectionView indexPathForItemAtPoint:self.showView.center];
//当前选中cell的位置,selectedIndexPath是通过第一步定位操作获取的,用于保存正在被拖动cell的indexPath
NSIndexPath *previousIndexPath = self.selectedIndexPath;
//第四步:判断showView移动到的target位置是否需要交换位置;
if (![self collectionView:_collectionView itemAtIndexPath:previousIndexPath canMoveToIndexPath:newIndexPath]) {
return;
}
//第五步:处理内存中的数据,交换数据位置;
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进行更新为新位置;
self.selectedIndexPath = newIndexPath;
}
第四步中的判断`showView`移动到目标位置是否需要交换位置的方法
- (BOOL)collectionView:(UICollectionView *)collectionView itemAtIndexPath:(NSIndexPath *)fromIndexPath canMoveToIndexPath:(NSIndexPath *)toIndexPath
{
//移动到显示分组的第一个位置时不需要交换位置
if (toIndexPath.section == 0) {
if (toIndexPath.row == 0) {
return NO;
}
}
else if (toIndexPath.section == 1){
//移动到未显示分组时不需要交换位置
return NO;
}
return YES;
}
>使用`iOS9`以后系统自带的新特性实现:
>手势状态为`Changed`时只需一句话,蛮简单!
CGPoint point = [gestureRecognizer locationInView:self.collectionView];
[self.collectionView updateInteractiveMovementTargetPosition:point];
长按手势拖动变化结束`Ended`:
>使用自定义方法实现:
>1、通过记录当前位置的`selectedIndexPath`获取当前位置的`frame`;
>2、将`showView`的`frame`置未第一步中获取的`frame`,模拟`cell`复位;
>3、清空辅助`showView`,并显示`cell`;
>```
-(void)dragItemEnd
{
//第一步:通过记录当前位置的selectedIndexPath获取当前位置的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复位
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
以后系统自带的新特性实现:
手势状态为Ended
时也只需一句话,蛮简单!
[self.collectionView endInteractiveMovement];
六、点击cell
实现加减功能
在编辑状态下点击显示分组的cell
,在显示分组中删除,将选中的cell
移动到未显示分组中,并且恢复到添加前的位置。点击未显示分组的cell
,在未显示分组中删除,将被选中的cell
添加到显示分组中;
主要思路:
1、需要实现分类标签数据的本地持久化存储,我将全部标签存在沙盒目录下的allSorts.plist
中,同时将已经显示的分类标签存在沙盒目录下的displaySort.plist
中,每次显示分类编辑页的时候将两组数据取出,对比两组数据获取未显示分类标签数据。并且实现总分类标签因服务器数据出现增减标签时与本地数据合并的功能,新分类标签在下次启动时显示;
1、合并服务器数据,解决数据增减问题:
/// 判断本地数据与服务端数据是否相同
/// @param locals 本地数据
/// @param remotes 服务器返回数据
- (BOOL)equalLocal:(NSArray *)locals remote:(NSArray *)remotes
{
//查询到在locals中,不在数组remotes中的数据
NSPredicate *localFilterPredicate = [NSPredicate predicateWithFormat:@"NOT (SELF IN %@)",remotes];
NSArray *localFilter = [locals filteredArrayUsingPredicate:localFilterPredicate];
[self.reduceObjects addObjectsFromArray:localFilter];
//查询到在remotes中,不在数组locals中的数据
NSPredicate *remoteFilterPredicate = [NSPredicate predicateWithFormat:@"NOT (SELF IN %@)",locals];
NSArray *remoteFilter = [remotes filteredArrayUsingPredicate:remoteFilterPredicate];
[self.addObjects addObjectsFromArray:remoteFilter];
//拼接数组,所有变化的数据,若array内容为空,则数组未改变
NSMutableArray *array = [NSMutableArray arrayWithArray:localFilter];
[array addObjectsFromArray:remoteFilter];
if (array.count>0) {
return YES;
}
return NO;
}
2、通过比较全部标签数据与已显示标签数据,获取未显示标签:
/// 获取未显示数据
- (NSArray *)getNoDisplayArray
{
NSPredicate *filterPredicate = [NSPredicate predicateWithFormat:@"NOT (SELF IN %@)",self.displayArray];
NSArray *resultArray = [self.allSortArray filteredArrayUsingPredicate:filterPredicate];
return resultArray;
}
3、处理分类更新数据变化,点击 cell
进行增删时使用:
这个方法在数据管理类中实现,将处理完的结果向UI层抛出回调,实现是数据与UI的分离
/**
处理分类更新数据变化
*/
- (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层中对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];
}];
}
七、相关代理回调
1、基础回调方法:
- (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
{
//在这里实现点击事件
}
2、只有使用iOS9
以后系统自带的新特性实现时使用:
- (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath*)destinationIndexPath
{
/**
* sourceIndexPath 原始数据 indexpath
* destinationIndexPath 移动到目标数据的 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滑动到Top分组第一个位置不移动,滑动到bottom分组任何位置不移动
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;
}
注:在通过全部标签数据和已显示标签数据比较获取未显示分类标签数据时使用了NSPredicate谓词查询,这里碰到了一个小问题,在UI层处理时数据都已经被处理为model实例化模型,导致了使用NSPredicate时没有过滤出想要的数据,所以我将数据处理放到了管理类中,在管理类中的数据还是原始模型(字典类型),这样再使用NSPredicate过滤时就没有出现,数据内容相同但地址不同时无法过滤的问题了; 建议使用自定义cell移动的方法实现分类标签编辑页,这样比使用iOS9以后系统提供的结构能更好的控制效果,并且兼容性更好。