iOS 高仿Timi记账

写在最前面:

其实本文应该早在两个月之前就该写完的,由于当时找工作,种种原因搁置到现在才写完。
本文没有把每一个点都能写到。再说时隔两个多月没碰这个项目了,多多少少都忘了一些。我尽可能把当时在写这个项目遇到的各种坑写详细。如果在看本文或者demo的时候有不明白的地方可以在Github上面提issue或者简书简信我也可以。
温馨提示:看文章的时候结合代码一起看,效果会更佳哟。
目前完成进度70%,由于时间的关系(临近期末,各种事情的原因...)
项目采用MVC设计模式
本人还属于菜鸟级别,代码写得不规范,望见谅!
如果项目中同样的问题,你有更好的办法解决请告诉我,让我们一起学习。

废话说了一大堆,开始进入正题!!!

项目视频演练 -> 点我

Demo ->Timi 不要忘记star支持哟

高仿版本:3.6.1

使用语言:Objective-C

开发工具及调试神器:Xcode 7.3.1,Reveal 1.6.3

用到的三方库及扩展库

Name Explain
Masonry 纯代码Autolayout
MBProgressHUD 未使用,后更改为使用SVProgressHUD
MMDrawerController 抽屉
SVProgressHUD HUD
YYText 著名库YYKit下的一个富文本
iCarousel 一个类似UIScrollView的控件
ColorCube 图片颜色提取
UITextView_PlaceHolder 给UITextView添加PlaceHolder
SZCalendarPicker 日历
TYPagerController 左右滚动ViewController VTMagic
Realm 移动端数据库新王者

数据库设计

TMBill(账单)

Key Identity Column Data Type length Allowed Null Default Description
√ √ billID NSString 64 主键
dateStr NSString 10 当前年月日 时间
reMarks NSString 40 nil 备注
remarkPhoto NSData √ nil 图片备注
isIncome BOOL 1 0 类型(收支)
money float 13 0 金额
FK category TMCategory 类别
FK book TMBooks 账本
TMBill(账单).png

TMCategory(类别)

Key Identity Column Data Type length Allowed Null Default Description
√ √ categoryID NSString 64 主键
categoryImageFileNmae NSString 64 类别icon文件名
categoryTitle NSString 3 类别标题
isIncome BOOL 1 类型(收支)
TMCategory(类别).png

TMBook(账本)

Key Identity Column Data Type length Allowed Null Default Description
√ √ bookID NSString 64 主键
bookName NSString 6 账本标题
imageIndex int 2 账本对应icon下标
bookImageFileName NSString 64 类别icon文件名
TMBook(账本).png

TMAddCategory(新增类别)

Key Identity Column Data Type length Allowed Null Default Description
√ √ categoryID NSString 64 主键
√ categoryImageFileNmae NSString 64 类别icon文件名
isIncome BOOL 1 类型(收支)
TMAddCategory(新增类别).png

TMCategory(类别),TMAddCategory(新增类别)都是采用plist表的方式先存储。当App每次启动的时候就会先检查数据库对应的表是否为空,为空则从plist表读取数据,存储到本地数据库。

项目整体结构

TimiStructure.png

温馨提醒

项目里面95%都是使用的纯代码方式布局(Masonry),如果不懂的Masonry纯代码布局的请先去了解一下。传送门=>串哥的深入讲解 AutoLayout 和 Masonry

时光轴界面(HomePageViewController)

2016-07-01 14.58.02.gif

UI布局之header部分(TMHeaderView)

Paste_Image.png

其实headerView部分没有什么好说的,那个饼图是用UIBezierPath和CAShapeLayer绘制而成,我把它单独封装出来了,因为在后面的饼图部分也用到了。关于饼图的加载数据时候的动画我是使用的CABasicAnimation具体的操作可以看demo的对应文件(TMPieView)

UI布局之数据显示部分(HomePageViewController | TMTimeLineCell)

Paste_Image.png

数据的显示全部在一个section里面,并没有分section显示,而且cell也只有一个样式,我是通过收支类型来判断的该那边显示数据。
时间轴上面,相同时间(同一天)时间label和金额label以及时间点不显示出来,我是在模型层加了一个BOOL变量来判断,同时在获取数据之后进行数据的重置,具体的操作可以看HomePageViewController的getDataAndResetBill函数。
然后在自定义cell(TMTimeLineCell)重写timeLineBill属性,通过判断来显示数据。
下图应该清楚的看懂整个cell的布局
Paste_Image.png

其实这种做法并不好,一个cell是能完成,但是代码看起来就有点乱糟糟的感觉,正确的做法是应该有两种样式的cell。分别是账单类型为收入,账单类型为支出两种样式。

很多人都应该碰到过,滑动tableView的时候Cell的数据会出现混乱,我是这样解决的,在自定义cell重写- (void)prepareForReuse函数,将cell里面的控件元素的属性和对象统统置为nil。

//* 解决tableView滚动导致数据混乱
     准备重用,防止滚动出现数据错乱 */
- (void)prepareForReuse {
    [super prepareForReuse];
    self.timeLineBill = nil;
    self.categoryImageBtn.imageView.image = nil;
    self.leftCategoryNameLabel.text = nil;
    self.leftMoneyLabel.text = nil;
    self.leftRemarkLabel.text = nil;
    self.rightCategoryNameLabel.text = nil;
    self.rightMoneyLabel.text = nil;
    self.rightRemarkLabel.text = nil;
    self.lastBill = NO;
    }

细心的人可能看到了我在下滑tableview的时候,中间的时光轴线也跟着变长。当我下滑到一定程度,然后松手就会push到新增账单界面,而且这个push动画不是系统自带的push动画。
下面我一一为大家解答:

时光轴的线条是怎么变长的?

第一步、我是新增的一个UIView,默认frame为(SCREEN_SIZE.width-1)/2,0 , 1, 0),将它加到tableview上面。

self.dropdownLineView = [[UIView alloc] initWithFrame:CGRectMake((SCREEN_SIZE.width-1)/2,0 , 1, 0)];
self.dropdownLineView.backgroundColor = LineColor;
[self.tableView addSubview:self.dropdownLineView];

第二步、在UIScrollViewDelegate的 - (void)scrollViewDidScroll:(UIScrollView *)scrollView 代理函数里面获取滑动的y值。判断其方向并重新设置dropdownLineView的frame即可

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    /** 当下拉的时候才有动画  y>0下拉,y<0上划*/
    CGFloat  y = [scrollView.panGestureRecognizer translationInView:self.tableView].y;
//    NSLog(@"%s--%d---y = %f",__func__,__LINE__,y);
    if (y>0) {
        /**
         *  疑问:为什么是`y`是`-y`不是`0`,因为`dropdownLineView`是添加到`tableView`的,所以当`tabelView`拉下的时候`dropdownLineView`也会跟着向下移动。
         *  当`y`是`-y`的时候`dropdownLineView`会向上移动`y`个单位,才会达到我们理想的效果
         */
        self.dropdownLineView.frame = CGRectMake((SCREEN_SIZE.width-1)/2, -y, 1, y);
        [self.tableView bringSubviewToFront:self.dropdownLineView];
         /** 饼图+号按钮动画*/
        [self.headerView animationWithCreateBtnDuration:1.0f angle:y];
    }
}

时光轴界面到添加账单(修改账单)界面的转场动画(LYPushTransition,LYPopTransition)

使用的是自定义的转场动画,具体如何使用请看喵神 和 KittenYang 的blog,推荐几句代码快速集成自定义转场效果+全手势驱动
1.首先定一个class,继承至NSObject,遵守UIViewControllerAnimatedTransitioning协议。
2.需要实现两个方法

/** 动画时间 */
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext

/** 转场动画内容(怎么转) */
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
Push代码细节讲解(是一个反向prensent转场动画)
/** 动画时间 */
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext {
    return 0.5f;
}
/** 动画内容(如何转场) */
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
    /**
     *
    1.transitionContext 过渡内容上下文,可以通过它调用`viewControllerForKey:`拿到对应的过渡控制器
        key:UITransitionContextToViewControllerKey 目的控制器
            UITransitionContextFromViewControllerKey 开始控制器
    2.拿到对应的过渡控制器之后需要设置view的frame
        `finalFrameForViewController:` 可以拿到最后的frame,最后即完成动画后的frame
        `initialFrameForViewController:` 拿到初始化的frame,开始动画之前的frame
    3.然后添加到`transitionContext的containerView`
    
    4.设置动画的其他附带属性动画
     
    5.做动画... `UIView的block动画`
     
    6.在动画结束后我们必须向context报告VC切换完成,是否成功。系统在接收到这个消息后,将对VC状态进行维护。
     *
     */
    
    //1...
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *toView = toVC.view;
    
    //2...
    CGRect finalFrame = [transitionContext finalFrameForViewController:toVC];
    //(dx, dy) eg:dx偏移多少
    toView.frame = CGRectOffset(finalFrame, 0, -SCREEN_SIZE.height);
    //3....
    UIView *containerView = [transitionContext containerView];
    [containerView addSubview:toView];
    
    //4...
    
    //5...
    [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^{
       toView.frame = finalFrame;
    } completion:^(BOOL finished) {
        //6...
        [transitionContext completeTransition:YES];
    }];
}
Pop做Push的相反操作即可

...

3. ViewController如何使用自定义转场动画
  • pushViewController
    在push的控制器设置navigationController的delegate为self

    self.navigationController.delegate = self;
    

    实现协议方法

    - (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                            animationControllerForOperation:(UINavigationControllerOperation)operation
                                                         fromViewController:(UIViewController *)fromVC
                                                           toViewController:(UIViewController *)toVC {
    if (operation == UINavigationControllerOperationPush) {
        LYPushTransition *push = [LYPushTransition new];
        return push;
    } else if (operation == UINavigationControllerOperationPop) {
        LYPopTransition *pop = [LYPopTransition new];
        return pop;
    }else {
        return nil;
    }
    

}
```
通过operation判断是push操作还是pop操作,然后然后对于的动画即可
pop控制器不需要做任何操作
如果使用push,则会发现NavigationBar没有变化,会一直处于那个地方,很丑...
然而使用present就可以避免这种现象

  • presentViewController
    设置presentViewController的ViewController的transitioningDelegate为self
    注意,如果是present的UINavigationController,则需要设置NavigationController的transitioningDelegate为self

    UIStoryboard *storyboard = [UIStoryboard    storyboardWithName:@"Main" bundle:nil];
    SecondViewController *secondVC = [storyboard instantiateViewControllerWithIdentifier:@"second"];
    secondVC.delegate = self;
    //* present */
    UINavigationController *navi = [[UINavigationController alloc] initWithRootViewController:secondVC];
    

//* 如果present的NavigationController则需要设置NavigationController的transitioningDelegate为self */
navi.transitioningDelegate = self;
[self presentViewController:navi animated:YES completion:nil];

实现`transitioningDelegate`协议方法

/** prensent */
  • (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController )presenting sourceController:(UIViewController )source {
    return self.push;
    }
    /
    dismiss */

  • (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
    return self.pop;
    }

    
      `dismiss`控制器则需要写一个代理,告诉`present`的那个控制器`dismiss`即可
      
      
      
    

NavigationItemTitleView按钮的边框&点击切换时候的颜色动画

/** 设置边框宽度 */
 titleBtn.layer.borderWidth = 1.5;
//* 设置Btn的边框颜色 */
titleBtn.layer.borderColor = [UIColor whiteColor].CGColor;

关于点击按钮切换时候的动画我是使用的两个UIView的动画

 //* 改变NavigationTitleBtn的颜色 */
    [UIView animateWithDuration:0.3f delay:0.2f options:UIViewAnimationOptionCurveEaseOut animations:^{
        [weakSelf.navigationTitleBtn setBackgroundColor:[UIColor colorWithRed:1.000 green:0.812 blue:0.124 alpha:1.000]];
    } completion:^(BOOL finished) {
        [UIView animateWithDuration:0.3f delay:0.0f options:UIViewAnimationOptionCurveEaseOut animations:^{
            [weakSelf.navigationTitleBtn setBackgroundColor:[UIColor colorWithWhite:0.278 alpha:0.500]];
        } completion:^(BOOL finished) {
            
        }];
    }];

点击类别按钮弹出菜单(TMTimeLineMenuView)

我不是在每个cell下面都添加了deleteBtn,updateBtn,因为这样会使性能大大降低。
我是自定义的一个UIView(TMTimeLineMenuView),这里面有三个控件,分别是deleteBtn,updateBtn,categoryBtn。
这个categoryBtn是放在deleteBtn,updateBtn上面的。因为在deleteBtn和updateBtn弹出的时候我把TMTimeLineMenuView放到了最顶层

//* 置顶 */
[weakSelf.superview bringSubviewToFront:weakSelf];

也就意味着tableView是在TMTimeLineMenuView的下面。


Paste_Image.png

如果没有categoryBtn,弹出deleteBtn和updateBtn就感觉是直接在tableViewCell上面做的动画,会很丑。所以添加一个categoryBtn放在updateBtn和deleteBtn上面,就感觉deleteBtn和updateBtn是放在tableViewCell下面的。给用户很好的用户体验。

如何将TMTimeLineMenuView中的控件显示到对应的位置?(HomePageViewController->didClickCategoryBtnWithIndexPath:)

第一步:获取到点击的cell对应的indexPath
第二步:获取对应cell在tableview中的rect
第三步:将获取到的rect转换成在self.view中的rect

/** 获取cell在tableView中的位置 */
CGRect rect = [self.tableView rectForRowAtIndexPath:indexPath];
//* 转换成在self.view中的位置 */
CGRect rectInSuperview = [self.tableView convertRect:rect toView:[self.tableView superview]];
self.timeLineMenuView.currentImage = self.timeLineCell.categoryImageBtn.currentImage;
[self.timeLineMenuView showTimeLineMenuViewWithRect:rectInSuperview ];

创建账单界面(TMCreateBillViewController)

TimiAddBillController.gif

选择类别动画之类别图片动画(应该使用UI Dynamics)

第一步:

在创建账单界面添加一个UIImageView控件,大小跟collectionViewCell里面的categoryImageView一样,放在屏幕外。并设置圆角。

- (UIImageView *)selectCategoryImageView
{
    if (!_selectCategoryImageView) {
        UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(0, -30, kCollectionCellWidth-20, kCollectionCellWidth-20)];
        imageView.layer.cornerRadius = (kCollectionCellWidth - 20)/2;
        imageView.layer.masksToBounds = YES;
        imageView.contentMode = UIViewContentModeScaleAspectFill;
        
        _selectCategoryImageView = imageView;
    }
    return _selectCategoryImageView;
}
第二步: 获取点击的位置

1.拿到对应cell

cell = (TMCategotyCollectionViewCell *)[collectionView cellForItemAtIndexPath:indexPath];

2.将cell对应的类别图片赋值给_selectCategoryImageView
然后获取到cell的center,这个centter的y仅仅是它在collectionView的位置,所以还需要修改y值,然后使用UIView的block动画移动到headerView上面对应的点。在动画完成之后将它放到最底层

/** 选择类别之后的类别图片动画 */
- (void)animationWithCell:(TMCategotyCollectionViewCell *)cell {
    self.selectCategoryImageView.image = cell.categoryImageView.image;
    CGPoint center = cell.center;
    /** 在collectionView中的y */
    CGFloat y =  CGRectGetMaxY(cell.frame);
    center.y = kMaxNBY + y + 10;
    self.selectCategoryImageView.center = center;
    WEAKSELF
    [UIView animateWithDuration:0.05 animations:^{
        weakSelf.selectCategoryImageView.center = kHeaderCategoryImageCenter;
    } completion:^(BOOL finished) {
        [weakSelf.view sendSubviewToBack:weakSelf.selectCategoryImageView];
    }];
    [self.view bringSubviewToFront:self.selectCategoryImageView];
}

选择类别动画之HeaderView颜色动画

第一步:提取颜色

我使用的是一个三方库,ColorExtraction

//* 颜色提取 */
CCColorCube *imageColor = [[CCColorCube alloc] init];
NSArray *colors = [imageColor extractColorsFromImage:category.categoryImage flags:CCAvoidBlack count:1];
第二步:动画

我是使用UIBezierPath和CAShapeLayer结合CABasicAnimation做的动画。
UIBezierPath的path如何而来?
path就是一条线,path的moveToPoint点就是self.bounds.origin点即左上点
addLineToPoint点就是self.bounds.origin.x点和self.bounds.size.height点即左下点
然后通过CABasicAnimation改变lineWidth

- (void)animationWithBgColor:(UIColor *)color {
    //* 如果选择的类别图片的颜色和上次选择的一样  直接return */
    if ([color isEqual: self.previousSelectColor]) return;
    //* 修改背景颜色为上一次选择的颜色,不然就会是最开始默认的颜色,动画会很丑,给用户的体验很不好 */
    self.backgroundColor = self.previousSelectColor;
    
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"lineWidth"];
    animation.fromValue = @0.0;
    animation.toValue = @(self.bounds.size.width * 2);
    animation.duration = 0.3f;
    //* 设置填充色 */
    self.bgColorlayer.fillColor = color.CGColor;
    //* 设置边框色 */
    self.bgColorlayer.strokeColor = color.CGColor;
    
    self.previousSelectColor = color;
    //* 保持动画 */
    animation.removedOnCompletion = NO;
    animation.fillMode = kCAFillModeForwards;
    
    [self.bgColorlayer addAnimation:animation forKey:@"bgColorAnimation"];

    //* 将子控件放在最上面,不然layer会覆盖 */
    [self bringSubviewToFront:self.categoryImageView];
    [self bringSubviewToFront:self.moneyLabel];
    [self bringSubviewToFront:self.categoryNameBtn];
}

饼图(TMPiewViewController)

TMPie.gif

饼图HeaderView部分

控件是使用三方库iCarousel 链接

数据源如何而来?

1.先把每个月中文对应的英文缩写保存到一个数组中

- (NSArray *)items {
    if (!_items) {
        _items = @[@"JAN\n1月",@"FEB\n2月",@"MAR\n3月",@"APR\n4月",@"MAY\n5月",@"JUN\n6月",@"JUL\n7月",@"AUG\n8月",@"SEP\n9月",@"OCT\n10月",@"NOV\n11月",@"DEC\n12月",@"ALL\n全部"];
    }
    return _items;
}

疑问:为什么数据每个元素,中间有个\n
答:我是使用的是一个UILabel\n用于换行
2.拿到筛选过后的数据,是一个NSDictionary。额...说一下,这个筛选过后的数据的一个结构,因为同一天我们可能会记多笔账,所以把同一天的dateStr作为key,然后把所有属于这一天的账单数据当作一个value,目前为止只是过滤掉同一天的时间字符串。
然后下一步我们要做的就是过滤掉同一年的相同月份

/** 过滤掉同年相同月份 */
- (void)filterMonthWithDateArray:(NSArray *)array {
    for (NSString *dateStr in array) {
        NSString *yearAndMonth = [dateStr substringToIndex:7];
        BOOL contains = [self containsMonth:yearAndMonth];
        if (!contains) {
            NSString *month = [self conversionDateStringIntoMonth:dateStr];
            [self.dic setValue:month forKey:dateStr];
        }
    }
    [self.dic setValue:self.items.lastObject forKey:@"ALL"];
    self.sortDicKeys = [self sortArray:self.dic.allKeys ascending:YES];
    [self.iCar reloadData];
}
/** 把时间字符串转换成月份 */
- (NSString *)conversionDateStringIntoMonth:(NSString *)dateString {
    NSRange range = NSMakeRange(5, 2);
    NSString *month = [dateString substringWithRange:range];
    return self.items[month.integerValue - 1];
}
/** 判断字典里面是否已经包含这个对象 */
- (BOOL)containsMonth:(NSString *)yearAndMonth {
    if (self.dic.allKeys.count==0) {
        return NO;
    } else {
        for (NSInteger i=0; i<self.dic.allKeys.count ; i++) {
            if ([[self.dic.allKeys[i] substringToIndex:7] isEqualToString:yearAndMonth]) {
                return YES;
            }
        }
    }
    return NO;
}

获取layer的位置

- (NSInteger)getLayerIndexWithPoint:(CGPoint)point {
    for (NSInteger i=0; i<[self.containerLayer sublayers].count; i++) {
        CAShapeLayer *layer = (CAShapeLayer *)[self.containerLayer sublayers][i];
        CGPathRef path = [layer path];
        if (CGPathContainsPoint(path, NULL, point, 0)) {
            return i;
        }
    }
    return -1;
}

拿到所有的sublayer,取出layer的path,通过CGPathContainsPoint判断触摸的点是否在这个path里面

类别详细界面(TMPiewCategoryDetailViewController)

解决cell重用导致数据年月日label显示混乱,在模型定义两个BOOL变量same,partSame
拿到数据之后将数据进行“重置”

 (void)resetBill {
    self.bills = [NSMutableArray array];
    NSString *previous;
    for (NSInteger i=0; i<self.results.count; i++) {
        TMBill *bill = self.results[i];
        if (i==0) {//第一个数据永远是不相同的
            [self.bills addObject:bill];
            previous = bill.dateStr;
            continue;
        } else {
            TMBill *theBill = [TMBill new];
            if ([previous isEqualToString:bill.dateStr]) {//完全相同,时间日期
                theBill = bill;
                theBill.same = YES;
                [self.bills addObject:theBill];
            } else if ([[previous substringToIndex:7] isEqualToString:[bill.dateStr substringToIndex:7]]) {//部分相同,年月份相同,具体时间不同
                theBill = bill;
                theBill.partSame = YES;
                [self.bills addObject:theBill];
            } else {//不同
                [self.bills addObject:bill];
            }
            previous = bill.dateStr;
        }
    }
}

侧滑控制器,使用的是MMDrawerController库

本来MMDrawerController是支持在屏幕向右滑就能出现左边的菜单栏,由于使用了TYPagerController出现了手势之间的冲突
解决和TYPagerController手势冲突的问题

UIScreenEdgePanGestureRecognizer *screenEdgeGR = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(clickMenuBtn:)];
    screenEdgeGR.edges = UIRectEdgeLeft;
    [self.view addGestureRecognizer:screenEdgeGR];
- (void)clickMenuBtn:(UIButton *)sender {
    [self.mm_drawerController toggleDrawerSide:MMDrawerSideLeft animated:YES completion:nil];
}

如果直接是这样的话则会出现下面的情况

2016-06-25 22.50.25.gif

因为UIScreenEdgePanGestureRecognizer是一个持续响应事件,也就是说你的手指没离开屏幕则会一直响应这个函数,因为toggleDrawerSide在内部会判断菜单栏是打开还是关闭,打开则关闭,关闭则会打开,所以也就会出现上面这种情况了。
解决办法

if (self.mm_drawerController.openSide == MMDrawerSideNone) {
        [self.mm_drawerController toggleDrawerSide:MMDrawerSideLeft animated:YES completion:nil];
    }

账本控制器(TMSideViewController)

books.gif

如何抖动?在cell上添加一个UILongPressGestureRecognizer长按手势

 //* 长按手势 */
UILongPressGestureRecognizer *longGR = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longGR:)];
longGR.minimumPressDuration = 1.0;
longGR.numberOfTouchesRequired = 1;
longGR.allowableMovement = 10;
[self addGestureRecognizer:longGR];

给cell添加一个代理

@protocol TMSideCellDelegate <NSObject>
@required
@optional
- (void)TMSideCellWithIndexPath:(NSIndexPath *)indexPath withLongPress:(UILongPressGestureRecognizer *)longPress;
@end

当控制器接收到响应事件的时候只需要做三件事

self.editSelectedIndexPath = indexPath;     //1
self.edit = YES;                            //2
[self.collectionView reloadData];           //3

在- (UICollectionViewCell *)collectionView: cellForItemAtIndexPath:添加判断代码

  //* edit mode on shake ->ture*/
    if (self.isEdit) {
        if ([indexPath isEqual:self.editSelectedIndexPath]) {
            cell.editSelectedItemImageView.hidden = NO;
            [self shakeCell:cell];
        } else {
            cell.editSelectedItemImageView.hidden = YES;
        }
    } else {
        cell.editSelectedItemImageView.hidden = YES;
        cell.transform = CGAffineTransformIdentity;
    }
/** 抖动动画 */
- (void)shakeCell:(TMSideCell *)cell {
    [UIView animateWithDuration:0.1 delay:0 options:0 animations:^{
        cell.transform=CGAffineTransformMakeRotation(-0.02);
    } completion:^(BOOL finished) {
        [UIView animateWithDuration:0.1 delay:0 options:UIViewAnimationOptionRepeat|UIViewAnimationOptionAutoreverse | UIViewAnimationOptionAllowUserInteraction animations:^{
            cell.transform=CGAffineTransformMakeRotation(0.02);
        } completion:nil];
    }];
}

好了,暂时就写这么多吧。有疑问可以在Github上面提issue或者简书简信我也可以。谢谢!

最后编辑于 :
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容