一、前言
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:实现方案中用到了截图的方案,在侧滑状态中,其实用的是一块截图
六、写在最后
对于以上的实现方案有更好建议的,欢迎指正,对于当前的效果,其实还有一些被不太完善,主要是动画那里,目前没有实现视图的滑动实时跟随手指移动,后续对于手势那部分可能还是要优化一下。