UIView 侧滑事件的封装

一、前言

iOS开发中,我们经常使用UITableViewCell的侧滑效果及事件。比如微信的主页面,都是可以侧滑的,然后里面是删除等功能。这是iOS设计规范里很通用的一种快捷操作方案,基本上每一款App都有这样的设计。那么问题来了,UIView有没有同样的处理方案呢?毕竟很多时候,UIView才是最常用的。

二、需求简要

视觉效果就是UITableViewCell的侧滑一样,功能也是那样。其实,实际开发中,UITableViewCell的侧滑用的也是第三方的控件,比如笔者现在用的就是MGSwipeTableCell,其效果与微信的一模一样(微信用的应该也是这个吧~)

三、应用场景

UITableView更多的用于列表展示页,与视图有频繁交互的页面还是要以UIView和相关控件为主,比如基本的表单页,需要承载用户输入的大量的数据,以及编辑等操作,这时,侧滑加入一下删除等操作就很常见了。

四、实现方案分析

在实现的过程中,小编在方案上纠结了一会儿,到底是用继承来实现还是Category呢?我第一想法是用继承,但最终我选择了Category。这里简单的说一下原因,小编我是从使用的角度来考虑的,如果使用的是继承,那么要使用的话,就意味着必须继承于这个封装的侧滑View,这样他们之间就绑定在了一起,而我很讨厌这种感觉,就是这种强依赖关系,况且,侧滑只是一种附带的小功能,能不能以“贴标签”的形式给target view 贴上swipe的功能呢?那么,我们很容易就想到了Cagegory。其实从实现的角度来考虑,继承的方案会简单很多,主要也是Category方案在实现中遇到了一些坑!

五、代码实现

UIView+Swipe.h

#import <UIKit/UIKit.h>

@interface SwipeButton : UIButton

+ (instancetype)buttonWithTitle:(NSString *)title
                backgroundColor:(UIColor *)backgroundColor
                       callback:(MPNoParameterBlock)handler;

@end

@interface UIView (Swipe)

/** 配置左侧的按钮 */
- (void)configLeftButtons:(NSArray<SwipeButton *> *)buttons swipe:(MPNoParameterBlock)swipeHandler;
/** 配置右侧的按钮 */
- (void)configRightButtons:(NSArray<SwipeButton *> *)buttons swipe:(MPNoParameterBlock)swipeHandler;
/** 从侧滑状态复位 */
- (void)recover;

@end

UIView+Swipe.m

#import "UIView+Swipe.h"
#import "Masonry.h"

@interface SwipeButton ()

@property (nonatomic, copy) MPNoParameterBlock useForRecoverHandler;

@property (nonatomic, copy) MPNoParameterBlock clickHandler;

@end

@implementation SwipeButton

+ (instancetype)buttonWithTitle:(NSString *)title
                backgroundColor:(UIColor *)backgroundColor
                       callback:(MPNoParameterBlock)handler {

    return [self buttonWithTitle:title icon:nil backgroundColor:backgroundColor callback:handler];
}

+ (instancetype)buttonWithTitle:(NSString *)title
                           icon:(UIImage*)icon
                backgroundColor:(UIColor *)backgroundColor
                       callback:(MPNoParameterBlock)callback {
    
    SwipeButton *button = [self buttonWithType:UIButtonTypeCustom];
    button.backgroundColor = backgroundColor;
    button.titleLabel.font = [UIFont systemFontOfSize:15];
    button.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
    button.titleLabel.textAlignment = NSTextAlignmentCenter;
    [button setTitle:title forState:UIControlStateNormal];
    [button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    [button setImage:icon forState:UIControlStateNormal];
    button.clickHandler = callback;
    [button sizeToFit];
    [button addTarget:self action:@selector(onClick:) forControlEvents:UIControlEventTouchUpInside];
    return button;
}

+ (void)onClick:(SwipeButton *)sender {
    if (sender.clickHandler) {
        sender.clickHandler();
    }
    if (sender.useForRecoverHandler) {
        sender.useForRecoverHandler();
    }
}

@end

typedef NS_ENUM(NSInteger, ViewSwipePosition) {
    ViewSwipePositionNormal = 0,    // 正常位置
    ViewSwipePositionLeft = 1,      // 左侧按钮出现
    ViewSwipePositionRight = 2,     // 右侧按钮出现
};

@interface UIView ()

@property (nonatomic, weak) UIImageView *mainShotImageView;

@property (nonatomic, weak) UIView *leftView;

@property (nonatomic, weak) UIView *rightView;

@property (nonatomic, assign) ViewSwipePosition position;

@property (nonatomic, assign) CGFloat leftSwipeDistance;

@property (nonatomic, assign) CGFloat rightSwipeDistance;

@property (nonatomic, copy) MPNoParameterBlock leftSwipeHandler;

@property (nonatomic, copy) MPNoParameterBlock rightSwipeHandler;

@end

#define widthPerItem 78

NSString *const leftViewKey = @"leftViewKey";
NSString *const rightViewKey = @"rightViewKey";
NSString *const positionKey = @"positionKey";
NSString *const leftSwipeDistanceKey = @"leftSwipeDistanceKey";
NSString *const rightSwipeDistanceKey = @"rightSwipeDistanceKey";
NSString *const mainShotImageViewKey = @"mainShotImageViewKey";
NSString *const rightSwipeHandlerKey = @"rightSwipeHandlerKey";
NSString *const leftSwipeHandlerKey = @"leftSwipeHandlerKey";


@implementation UIView (Swipe)

- (void)setLeftView:(UIView *)leftView {
    objc_setAssociatedObject(self, &leftViewKey, leftView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (UIView *)leftView {
    return objc_getAssociatedObject(self, &leftViewKey);
}

- (void)setRightView:(UIView *)rightView {
    objc_setAssociatedObject(self, &rightViewKey, rightView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (UIView *)rightView {
    return objc_getAssociatedObject(self, &rightViewKey);
}

- (void)setMainShotImageView:(UIImageView *)mainShotImageView {
    objc_setAssociatedObject(self, &mainShotImageViewKey, mainShotImageView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (UIImageView *)mainShotImageView {
    return objc_getAssociatedObject(self, &mainShotImageViewKey);
}

- (void)setPosition:(ViewSwipePosition)position {
    NSNumber *positionNum = [NSNumber numberWithInteger:position];
    objc_setAssociatedObject(self, &positionKey, positionNum, OBJC_ASSOCIATION_ASSIGN);
}

- (ViewSwipePosition)position {
    NSNumber *isRightNum = objc_getAssociatedObject(self, &positionKey);
    return isRightNum.theIntegerValue;
}

- (void)setLeftSwipeDistance:(CGFloat)leftSwipeDistance {
    NSNumber *swipeDistanceNum = [NSNumber numberWithFloat:leftSwipeDistance];
    objc_setAssociatedObject(self, &leftSwipeDistanceKey , swipeDistanceNum, OBJC_ASSOCIATION_ASSIGN);
}

- (CGFloat)leftSwipeDistance {
    NSNumber *swipeDistanceNum = objc_getAssociatedObject(self, &leftSwipeDistanceKey);
    return swipeDistanceNum.theFloatValue;
}

- (void)setRightSwipeDistance:(CGFloat)rightSwipeDistance {
    NSNumber *swipeDistanceNum = [NSNumber numberWithFloat:rightSwipeDistance];
    objc_setAssociatedObject(self, &rightSwipeDistanceKey, swipeDistanceNum, OBJC_ASSOCIATION_ASSIGN);
}

- (CGFloat)rightSwipeDistance {
    NSNumber *swipeDistanceNum = objc_getAssociatedObject(self, &rightSwipeDistanceKey);
    return swipeDistanceNum.theFloatValue;
}

- (MPNoParameterBlock)leftSwipeHandler {
    MPNoParameterBlock block = objc_getAssociatedObject(self, &leftSwipeHandlerKey);
    return block;
}

- (void)setLeftSwipeHandler:(MPNoParameterBlock)leftSwipeHandler {
    objc_setAssociatedObject(self, &leftSwipeHandlerKey, leftSwipeHandler, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (MPNoParameterBlock)rightSwipeHandler {
    MPNoParameterBlock block = objc_getAssociatedObject(self, &rightSwipeHandlerKey);
    return block;
}

- (void)setRightSwipeHandler:(MPNoParameterBlock)rightSwipeHandler {
    objc_setAssociatedObject(self, &rightSwipeHandlerKey, rightSwipeHandler, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (void)configMainImageView {
    
    if (self.mainShotImageView) {
        return;
    }
    UIImageView *mainImageView = [[UIImageView alloc] init];
    mainImageView.hidden = YES;
    mainImageView.userInteractionEnabled = YES;
    [self addSubview:mainImageView];
    self.mainShotImageView = mainImageView;
    [mainImageView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.mas_equalTo(0);
        make.top.bottom.mas_equalTo(self);
        make.right.mas_equalTo(0);
    }];
}

- (void)configLeftButtons:(NSArray<SwipeButton *> *)buttons
                    swipe:(MPNoParameterBlock)swipeHandler {

    if (buttons.count == 0) {
        return;
    }
    if (swipeHandler) {
        self.leftSwipeHandler = swipeHandler;
    }
    UIView *leftView = [[UIView alloc] init];
    leftView.backgroundColor = [UIColor whiteColor];
    [self addSubview:leftView];
    self.leftView = leftView;
    
    [self configMainImageView];
    
    self.leftSwipeDistance = widthPerItem * buttons.count;
    [leftView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.right.mas_equalTo(self.mainShotImageView.mas_left);
        make.top.bottom.mas_equalTo(self);
        make.width.mas_equalTo(0);
    }];
    
    [self configLeftSlipGesture];
    
    SwipeButton *currentBtn;
    __weak typeof(self)weakSelf = self;
    for (SwipeButton *btnItem in buttons) {
        [self.leftView addSubview:btnItem];
        [btnItem mas_makeConstraints:^(MASConstraintMaker *make) {
            make.right.mas_equalTo(currentBtn ? currentBtn.mas_left : self.leftView.mas_right);
            make.top.bottom.mas_equalTo(self.leftView);
            if (currentBtn) {
                make.width.mas_equalTo(currentBtn);
            }
        }];
        currentBtn = btnItem;
        currentBtn.useForRecoverHandler = ^{
            [weakSelf recoverSwipeState:UISwipeGestureRecognizerDirectionLeft];
        };
    }
    if (currentBtn) {
        [currentBtn mas_makeConstraints:^(MASConstraintMaker *make) {
            make.left.mas_equalTo(self.leftView);
        }];
    }
}

- (void)configRightButtons:(NSArray<SwipeButton *> *)buttons
                     swipe:(MPNoParameterBlock)swipeHandler {
    
    if (buttons.count == 0) {
        return;
    }
    if (swipeHandler) {
        self.rightSwipeHandler = swipeHandler;
    }
    UIView *rightView = [[UIView alloc] init];
    rightView.backgroundColor = [UIColor whiteColor];
    [self addSubview:rightView];
    self.rightView = rightView;
    
    [self configMainImageView];
    
    self.rightSwipeDistance = widthPerItem * buttons.count;
    [rightView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.mas_equalTo(self.mainShotImageView.mas_right);
        make.top.bottom.mas_equalTo(self);
        make.width.mas_equalTo(0);
    }];
    
    [self configLeftSlipGesture];
    
    SwipeButton *currentBtn;
    __weak typeof(self)weakSelf = self;
    for (SwipeButton *btnItem in buttons) {
        [self.rightView addSubview:btnItem];
        [btnItem mas_makeConstraints:^(MASConstraintMaker *make) {
            make.left.mas_equalTo(currentBtn ? currentBtn.mas_right : self.rightView.mas_left);
            make.top.bottom.mas_equalTo(self.rightView);
            if (currentBtn) {
                make.width.mas_equalTo(currentBtn);
            }
        }];
        currentBtn = btnItem;
        currentBtn.useForRecoverHandler = ^{
            [weakSelf recoverSwipeState:UISwipeGestureRecognizerDirectionRight];
        };
    }
    if (currentBtn) {
        [currentBtn mas_makeConstraints:^(MASConstraintMaker *make) {
            make.right.mas_equalTo(self.rightView);
        }];
    }
}

- (void)recover {
    
    if (self.position == ViewSwipePositionLeft) {
        [self recoverSwipeState:UISwipeGestureRecognizerDirectionLeft];
    } else if (self.position == ViewSwipePositionRight) {
        [self recoverSwipeState:UISwipeGestureRecognizerDirectionRight];
    }
}

- (void)recoverSwipeState:(UISwipeGestureRecognizerDirection)direction {
    
    UISwipeGestureRecognizer *gesture = [[UISwipeGestureRecognizer alloc] init];
    gesture.direction = direction;
    [self handleSwipeFrom:gesture];
}

- (void)configLeftSlipGesture {
    
    for (UIGestureRecognizer *gesture in self.gestureRecognizers) {
        [self removeGestureRecognizer:gesture];
    }
    for (UIGestureRecognizer *gesture in self.mainShotImageView.gestureRecognizers) {
        [self.mainShotImageView removeGestureRecognizer:gesture];
    }
    
    UISwipeGestureRecognizer *recognizerLeft = [[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(handleSwipeFrom:)];
    [recognizerLeft setDirection:UISwipeGestureRecognizerDirectionLeft];
    [self addGestureRecognizer:recognizerLeft];
    
    UISwipeGestureRecognizer *mainShotRecognizerLeft = [[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(handleSwipeFrom:)];
    [mainShotRecognizerLeft setDirection:UISwipeGestureRecognizerDirectionLeft];
    [self.mainShotImageView addGestureRecognizer:mainShotRecognizerLeft];

    UISwipeGestureRecognizer *recognizerRight = [[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(handleSwipeFrom:)];
    [recognizerRight setDirection:UISwipeGestureRecognizerDirectionRight];
    [self addGestureRecognizer:recognizerRight];
    
    UISwipeGestureRecognizer *mainShotRecognizerRight = [[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(handleSwipeFrom:)];
    [mainShotRecognizerRight setDirection:UISwipeGestureRecognizerDirectionRight];
    [self.mainShotImageView addGestureRecognizer:mainShotRecognizerRight];
}



- (void)handleSwipeFrom:(UISwipeGestureRecognizer *)sender {
    
    if (self.position == ViewSwipePositionNormal) {
        if (sender.direction == UISwipeGestureRecognizerDirectionLeft && self.rightView) {
            self.mainShotImageView.image = [self screenShotView:self];
            self.mainShotImageView.hidden = NO;
            [self updateSelfPostionTo:ViewSwipePositionRight];
            if (self.rightSwipeHandler) {
                self.rightSwipeHandler();
            }
        } else if (sender.direction == UISwipeGestureRecognizerDirectionRight && self.leftView) {
            self.mainShotImageView.image = [self screenShotView:self];
            self.mainShotImageView.hidden = NO;
            [self updateSelfPostionTo:ViewSwipePositionLeft];
            if (self.leftSwipeHandler) {
                self.leftSwipeHandler();
            }
        }
    } else if (self.position == ViewSwipePositionLeft) {
        if (sender.direction == UISwipeGestureRecognizerDirectionLeft) {
            [self updateSelfPostionTo:ViewSwipePositionNormal];
        }
    } else if (self.position == ViewSwipePositionRight) {
        if (sender.direction == UISwipeGestureRecognizerDirectionRight) {
            [self updateSelfPostionTo:ViewSwipePositionNormal];
        }
    }
}

- (void)updateSelfPostionTo:(ViewSwipePosition)position {
    
    CGFloat offset = 0;
    if (position == ViewSwipePositionRight) {
        offset = self.rightSwipeDistance;
    } else if (position == ViewSwipePositionLeft) {
        offset = -self.leftSwipeDistance;
    }
    
    [self.mainShotImageView mas_updateConstraints:^(MASConstraintMaker *make) {
        make.left.mas_equalTo(-offset);
        make.right.mas_equalTo(self.mas_right).offset(-offset);
    }];
    
    if (position == ViewSwipePositionLeft) {
        [self.leftView mas_updateConstraints:^(MASConstraintMaker *make) {
            make.width.mas_equalTo(-offset);
        }];
    } else if (position == ViewSwipePositionRight) {
        [self.rightView mas_updateConstraints:^(MASConstraintMaker *make) {
            make.width.mas_equalTo(offset);
        }];
    }
    
    
    [self configAndActionAnimationIfNeed];
    
    self.position = position;

    if (self.position == ViewSwipePositionNormal) {
        dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 0.3 * NSEC_PER_SEC);
        dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
            self.mainShotImageView.hidden = YES;
        });
    }
}

// 对指定视图进行截图
- (UIImage *)screenShotView:(UIView *)view {
    
    UIImage *imageRet = nil;
    if (view) {
        UIGraphicsBeginImageContextWithOptions(view.frame.size, NO, 0.0);
        
        //获取图像
        [view.layer renderInContext:UIGraphicsGetCurrentContext()];
        imageRet = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
    } else {
        
    }
    return imageRet;
}

@end

Usage

SwipeButton *deleteBtn = [SwipeButton buttonWithTitle:@"删除" backgroundColor:RedColor callback:^{
    // Click Handler
}];
[view configRightButtons:@[deleteBtn] swipe:nil];

Tips:实现方案中用到了截图的方案,在侧滑状态中,其实用的是一块截图

六、写在最后

对于以上的实现方案有更好建议的,欢迎指正,对于当前的效果,其实还有一些被不太完善,主要是动画那里,目前没有实现视图的滑动实时跟随手指移动,后续对于手势那部分可能还是要优化一下。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_X自主阅读 15,969评论 3 119
  • “叮咚,叮咚……” “来了,来了。”门铃响起第三遍时,一个急匆匆的声音从门缝里传了出来。 “梅梅!你怎么来了?也没...
    无色口红阅读 761评论 0 6
  • 题目:一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。 解法一: 这种题...
    qming_c阅读 240评论 0 0
  • 记得从前在某本笔记本的底角记下过一句话“心湖蓄满了,该从笔尖溢出了”,现在的我不正是这种状态吗?总会不由自主地想把...
    170415阅读 494评论 1 6
  • 每个人都有一个坚硬的盔甲,怕受伤害,用一个硬壳把自己包裹起来。
    若兰词话阅读 309评论 0 1