SWTableViewCell-源码分析与仿写(一)

前言

阅读优秀的开源项目是提高编程能力的有效手段,我们能够从中开拓思维、拓宽视野,学习到很多不同的设计思想以及最佳实践。阅读他人代码很重要,但动手仿写、练习却也是很有必要的,它能进一步加深我们对项目的理解,将这些东西内化为自己的知识和能力。然而真正做起来却很不容易,开源项目阅读起来还是比较困难,需要一些技术基础和耐心。
本系列将对一些著名的iOS开源类库进行深入阅读及分析,并仿写这些类库的基本实现,加深我们对底层实现的理解和认识,提升我们iOS开发的编程技能。

SWTableViewCell

SWTableViewCell是UITableViewCell的子类,它具有左右滑动显示操作菜单的功能。很多APP都有这个功能,比如微信列表页往左侧滑动显示操作菜单,可以删除或标为未读。我们看一下它的效果:


SWTableViewCell的地址是:https://github.com/CEWendel/SWTableViewCell,为了更好理解它的实现过程,避免被一些细节干扰,仍然使用简单的早期版本,v0.1.1版,接下来我们看看它是如何实现的。

实现原理

SWTableViewCell是一个继承自UITableViewCell的自定义Cell,它上面放了一个UIScrollerView,这个滚动视图上放了Cell内容、左侧操作菜单和右侧操作菜单。正常情况下,显示cell内容,当往左侧滑动时,滚动视图往左移动,显示右侧的操作菜单,右滑同理。
左、右操作菜单上放置一些操作按钮,由使用者配置,包括按钮的数量,样式,位置等。这些按钮的事件统一回调给使用者,由使用者指定具体实现。

实现过程

SWUtilityButtonView

左右两侧的操作菜单类,管理操作按钮的布局、事件回调。
在SWUtilityButtonView类中,有以下属性

@property (nonatomic, strong) NSArray *utilityButtons;//存放操作按钮的数组
@property (nonatomic) CGFloat utilityButtonWidth;//操作按钮的宽度
@property (nonatomic, weak) SWTableViewCell *parentCell;//操作按钮所在的cell
@property (nonatomic) SEL utilityButtonSelector;//操作按钮点击事件

其中utilityButtonWidth表示操作按钮的宽度,默认是90。当操作菜单有过多的按钮时,该值将重新计算取均分值,避免按钮太多撑满整个cell。 utilityButtonSelector是操作按钮的点击事件,该事件不在SWUtilityButtonView处理,而是要传递到parentCell中,即操作按钮的点击事件传递到上层cell中。parentCell还有个作用,取得cell的高度给SWUtilityButtonView。
计算每个操作按钮的实际宽度
- (CGFloat)calculateUtilityButtonWidth {
CGFloat buttonWidth = kUtilityButtonWidthDefault;
if (buttonWidth * _utilityButtons.count > kUtilityButtonsWidthMax) {
CGFloat buffer = (buttonWidth * _utilityButtons.count) - kUtilityButtonsWidthMax;
buttonWidth -= (buffer / _utilityButtons.count);
}
return buttonWidth;
}
操作按钮在页面上布局,以及配置事件响应方,通过tag属性标识每个按钮
- (void)populateUtilityButtons {
NSUInteger utilityButtonsCounter = 0;
for (UIButton *utilityButton in _utilityButtons) {
CGFloat utilityButtonXCord = 0;
if (utilityButtonsCounter >= 1) utilityButtonXCord = _utilityButtonWidth * utilityButtonsCounter;
[utilityButton setFrame:CGRectMake(utilityButtonXCord, 0, _utilityButtonWidth, CGRectGetHeight(self.bounds))];
[utilityButton setTag:utilityButtonsCounter];
[utilityButton addTarget:_parentCell action:_utilityButtonSelector forControlEvents:UIControlEventTouchDown];
[self addSubview: utilityButton];
utilityButtonsCounter++;
}
}

NSMutableArray+SWUtilityButtons

可变数组的扩展,提供了生成指定样式的操作按钮的功能 。
NSMutableArray+SWUtilityButtons类提供了两个初始化操作按钮的方法
@interface NSMutableArray (SWUtilityButtons)

- (void)sw_addUtilityButtonWithColor:(UIColor *)color title:(NSString *)title;
- (void)sw_addUtilityButtonWithColor:(UIColor *)color icon:(UIImage *)icon;

@end

其实很简单
- (void)sw_addUtilityButtonWithColor:(UIColor *)color title:(NSString *)title {
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.backgroundColor = color;
[button setTitle:title forState:UIControlStateNormal];
[button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[self addObject:button];
}
需要注意的是SWUtilityButtonView初始化和调用过程有一定的顺序,不能搞反了。

    //1,初始化
    [scrollViewButtonViewRight setFrame:CGRectMake(CGRectGetWidth(self.bounds), 0, [self rightUtilityButtonsWidth], _height)];
    //2,添加到滚动视图上
    [self.cellScrollView addSubview:scrollViewButtonViewRight];

    //3,操作按钮布局与事件回调设置
    [scrollViewButtonViewLeft populateUtilityButtons];
    [scrollViewButtonViewRight populateUtilityButtons];

SWTableViewCell

滑动显示菜单cell,它统一管理操作菜单的生成、事件处理、响应回调等。
cell上的滚动视图的初始化,它的contentSize是左侧操作菜单的加cell的宽度加右侧操作菜单的宽度

    UIScrollView *cellScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.bounds), _height)];
    cellScrollView.contentSize = CGSizeMake(CGRectGetWidth(self.bounds) + [self utilityButtonsPadding], _height);
    cellScrollView.contentOffset = [self scrollViewContentOffset];
    cellScrollView.delegate = self;
    cellScrollView.showsHorizontalScrollIndicator = NO;
    cellScrollView.scrollsToTop = NO;

将原来cell上的内容添加到滚动视图上

    UIView *contentViewParent = self;
    if (![NSStringFromClass([[self.subviews objectAtIndex:0] class]) isEqualToString:kTableViewCellContentView]) {
        // iOS 7
        contentViewParent = [self.subviews objectAtIndex:0];
    }
    NSArray *cellSubviews = [contentViewParent subviews];
    [self insertSubview:cellScrollView atIndex:0];
    for (UIView *subview in cellSubviews) {
        [self.scrollViewContentView addSubview:subview];
    }

当左右滑动cell时,实际上是根据滑动范围控制显示相应左右侧操作菜单
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView.contentOffset.x > [self leftUtilityButtonsWidth]) {
// 显示右侧操作菜单
self.scrollViewButtonViewRight.frame = CGRectMake(scrollView.contentOffset.x + (CGRectGetWidth(self.bounds) - [self rightUtilityButtonsWidth]), 0.0f, [self rightUtilityButtonsWidth], _height);
} else {
// 显示左侧操作菜单
self.scrollViewButtonViewLeft.frame = CGRectMake(scrollView.contentOffset.x, 0.0f, [self leftUtilityButtonsWidth], _height);
}
}
根据用户滑动的力度,显示相应的操作菜单。如果用户滑动范围不足操作菜单宽度的一半,cell回到正常状态,超过时,则滑动到相应的操作菜单
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
switch (_cellState) {
case kCellStateCenter:
if (velocity.x >= 0.5f) {//滑动力度
[self scrollToRight:targetContentOffset];
} else if (velocity.x <= -0.5f) {
[self scrollToLeft:targetContentOffset];
} else {
CGFloat rightThreshold = [self utilityButtonsPadding] - ([self rightUtilityButtonsWidth] / 2);
CGFloat leftThreshold = [self leftUtilityButtonsWidth] / 2;
if (targetContentOffset->x > rightThreshold)//滑动范围超过操作菜单宽度的一半,显示操作菜单栏
[self scrollToRight:targetContentOffset];
else if (targetContentOffset->x < leftThreshold)
[self scrollToLeft:targetContentOffset];
else
[self scrollToCenter:targetContentOffset];
}
break;
case kCellStateLeft:
if (velocity.x >= 0.5f) {
[self scrollToCenter:targetContentOffset];
} else if (velocity.x <= -0.5f) {
// No-op
} else {
if (targetContentOffset->x >= ([self utilityButtonsPadding] - [self rightUtilityButtonsWidth] / 2))
[self scrollToRight:targetContentOffset];
else if (targetContentOffset->x > [self leftUtilityButtonsWidth] / 2)
[self scrollToCenter:targetContentOffset];
else
[self scrollToLeft:targetContentOffset];
}
break;
case kCellStateRight:
if (velocity.x >= 0.5f) {
// No-op
} else if (velocity.x <= -0.5f) {
[self scrollToCenter:targetContentOffset];
} else {
if (targetContentOffset->x <= [self leftUtilityButtonsWidth] / 2)
[self scrollToLeft:targetContentOffset];
else if (targetContentOffset->x < ([self utilityButtonsPadding] - [self rightUtilityButtonsWidth] / 2))
[self scrollToCenter:targetContentOffset];
else
[self scrollToRight:targetContentOffset];
}
break;
default:
break;
}
}
操作按钮的响应事件传递到cell中,通过tag判断当前点击的按钮。
- (void)rightUtilityButtonHandler:(id)sender {
UIButton *utilityButton = (UIButton *)sender;
NSInteger utilityButtonTag = [utilityButton tag];
if ([_delegate respondsToSelector:@selector(swippableTableViewCell:didTriggerRightUtilityButtonWithIndex:)]) {
[_delegate swippableTableViewCell:self didTriggerRightUtilityButtonWithIndex:utilityButtonTag];
}
}
整个类库的实现过程大致就是这些,基本实现思路就是在scrollView上滑动显示菜单区。下面我们自己仿写一下这个类库,以加深我们对它内部实现的理解和掌握。为了简单起见,我们只实现基本的功能,一些细节都忽略掉了。

仿写SWTableViewCell

首先创建ZCJButtonView,存放操作按钮,设置这些按钮的回调,主要方法如下:
- (id)initWithFrame:(CGRect)frame Buttons:(NSArray *)buttons parentCell:(ZCJTableViewCell *)parentCell buttonSelector:(SEL)buttonSelector{
self = [super initWithFrame:frame];
if (self) {
_buttons = buttons;
_parentCell = parentCell;
_buttonSelector = buttonSelector;
}
return self;
}

//计算自身的总宽度
- (CGFloat)getWidth {
    return KButtonWidthDefault * _buttons.count;
}

- (void)layoutButtons {
    NSInteger buttonCount = 1000;
    for (UIButton *button in _buttons) {
        button.frame = CGRectMake((buttonCount-1000)*KButtonWidthDefault, 0, KButtonWidthDefault, self.bounds.size.height);
        button.tag = buttonCount;
        [button addTarget:_parentCell action:_buttonSelector forControlEvents:UIControlEventTouchUpInside];
        [self addSubview:button];
        buttonCount++;
    }
}

NSMutableArray+ZCJButtons类,我们只写一个初始化操作按钮的方法
- (void)addButtonWithBackgroundColor:(UIColor *)color withTitle:(NSString *)title {
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
[btn setTitle:title forState:UIControlStateNormal];
[btn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[btn setBackgroundColor:color];
[self addObject:btn];
}
ZCJTableViewCell简化了很多功能,这里只处理了右侧滑动,类的初始化方法如下
- (void)initializar {
//右侧操作视图初始化
ZCJButtonView *rightButtonView = [[ZCJButtonView alloc] initWithButtons:_rightButtons parentCell:self buttonSelector:@selector(buttonAction:)];
rightButtonView.frame = CGRectMake(self.bounds.size.width, 0, [_rightButtonView getWidth], _height);
[rightButtonView layoutButtons];
_rightButtonView = rightButtonView;

    //滚动视图初始化
    UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, self.bounds.size.width, _height)];
    scrollView.contentSize = CGSizeMake(self.bounds.size.width + [rightButtonView getWidth], self.height);
    scrollView.delegate = self;
    scrollView.showsHorizontalScrollIndicator = NO;
    scrollView.scrollsToTop = NO;
    _cellScrollView = scrollView;

    [_cellScrollView addSubview:_rightButtonView];

    UIView *contentView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.bounds.size.width, _height)];
    contentView.backgroundColor = [UIColor whiteColor];
    [_cellScrollView addSubview:contentView];
    _cellContentView = contentView;

    //将原来cell上的内容添加到滚动视图上
    UIView *contentViewParent = self;
    if (![NSStringFromClass([[self.subviews objectAtIndex:0] class]) isEqualToString:kTableViewCellContentView]) {
        // iOS 7
        contentViewParent = [self.subviews objectAtIndex:0];
    }
    NSArray *cellSubviews = [contentViewParent subviews];
    [self insertSubview:_cellScrollView atIndex:0];
    for (UIView *subview in cellSubviews) {
        [_cellContentView addSubview:subview];
    }
}

滑动cell时,操作菜单栏开始慢慢出现

-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
    if (scrollView.contentOffset.x > 0) {
        self.rightButtonView.frame = CGRectMake(scrollView.contentOffset.x + self.bounds.size.width - [self.rightButtonView getWidth], 0, [self.rightButtonView getWidth], _height);
    }
}

快速滑动cell时完成显示操作菜单栏。当只少量滑动时,恢复到cell正常状态
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
switch (_state) {
case ZCJCellStateCenter:
if (velocity.x >= 0.5) {
[self scrollToRight:targetContentOffset];
}
else {
if (targetContentOffset->x >= [self.rightButtonView getWidth] / 2) {
[self scrollToRight:targetContentOffset];
}
else {
[self scrollToCenter:targetContentOffset];
}
}
break;

        case ZCJCellStateRight:
            if (velocity.x >= 0.5) {
            }
            else if (velocity.x <= -0.5) {
                [self scrollToCenter:targetContentOffset];
            }
            else {
                if (targetContentOffset->x <= [self.rightButtonView getWidth] / 2) {
                    [self scrollToCenter:targetContentOffset];
                }
                else {
                    [self scrollToRight:targetContentOffset];
                }
            }
            break;
        default:
            break;
    }
}

操作按钮的点击事件传递到cell上进行处理
- (void)buttonAction:(id)sender {
UIButton *btn = sender;
NSInteger index = btn.tag - 1000;
if ([self.delegate respondsToSelector:@selector(swippableTableViewCell:didTriggerRightButtonViewWithIndex:)]) {
[self.delegate swippableTableViewCell:self didTriggerRightButtonViewWithIndex:index];
}
}
仿写的ZCJTableViewCell源码在这里:https://github.com/superzcj/ZCJTableViewCell

总结

SWTableViewCell是一个很棒的自定义cell,它的实现给我们很多启发,在我们日常编写自定义view中有很多可以学习的地方,比如SEL事件往上层传递,scrollView的使用。阅读这个类库的实现方式也让我受益匪浅,我也会在今后继续用这种方式阅读和仿写其它的著名类库,希望大家多多支持。
文章中难免有错误有不足,希望大家多多指正。

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

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,093评论 4 62
  • 被这个世界背叛的我 在泥泞的深渊里辗转 只为遇见下一个明天 黑的夜 黑的光 什么都看不到 也什么都不想看到 ...
    猫尾巴花阅读 231评论 0 0
  • 一场夜晚过去,迎来了阳光明媚的今天,今天的阳光格外的耀眼,都把我的眼睛给刺的不之所错了. 好吧,就在这阳光明媚的早...
    是代打还阅读 213评论 0 0
  • 感谢小灶群友@古怪兔的推荐,这是看简七理财视频整理的理财资料,总共9课,剩下的6课后续跟进。 一、理财是什么 理财...
    好听的暖阳阅读 761评论 2 2