制作一个可以滑动操作的 Table View Cell
原文地址:https://github.com/nixzhu/dev-blog/blob/master/2014-04-26-make-swipeable-table-view-cell-actions-without-going-nuts-scroll-views.md Apple 通过 iOS 7 的邮件(Mail)应用介绍了一种新的用户界面方案——向左滑动以显示一个有着多个操作的菜单。本教程将会向你展示如何制作一个这样的 Table View Cell,而不用因嵌套的 Scroll View 陷入困境。如果你还不知道一个可滑动的 Table View Cell 意味着什么,那么看看 Apple 的邮件应用:
可能你会想,既然 Apple 展示了这种方案,那它应该已将其开放给开发者使用了。毕竟,这能有多难呢?但不幸的是,他们只让开发者使用 Delete 按钮——至少暂时是这样。如果你要添加其他的按钮,或者改变 Delete 按钮上的文字或颜色,那你就必须自己去实现。 译者注:其实文字是可以修改的,但是颜色真的不行! 在本教程中,你将先学习如何实现简单的滑动以删除操作(swipe-to-delete action),之后我们再实现滑动以执行操作(swipe-to-perform-actions)。这会要求你深入研究 iOS 7 开始 打开 Xcode,去往 将项目命名为 对于这样的概念项目的证明,你最好保证数据模型尽量简单。 打开 - (void)viewDidLoad {
[super viewDidLoad];
//1
_objects = [NSMutableArray array];
//2
NSInteger numberOfItems = 30;
for (NSInteger i = 1; i <= numberOfItems; i++) {
NSString *item = [NSString stringWithFormat:@"Item #%d",i];
[_objects addObject:item];
}
}
这个方法做了两件事:
下一步。找到 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
NSString *item = _objects[indexPath.row];
cell.textLabel.text = item;
return cell;
}
原本 往下滚动到 就在这个方法下边, 用下面的代码替换 void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
[_objects removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
} else {
NSLog(@"Unhandled editing style! 当用户删除某行时,你就用传入的 Index 将那一行的对象从后面的数组中移除,并告知 Table View 它需要移除同一个 indexPath 所表示的那一行 Cell,一确保模型和视图的匹配。
你的应用只允许“delete”这一种编辑方式,但在 else 分支里用 log 记录你没有在处理什么也不错。如果有某个诡异的事情发生,你将会在控制台得到一个提示消息,这比方法静悄悄地返回要好。 最后,还有一些清理要做。依然在 编译并运行应用;你会看到一个简单列表,如下所示: 滑动某一行到左边,你就会看到一个 “Delete” 按钮,如下所示: 喔~——这很简单。但现在是时候弄脏双手,深挖进视图层次结构,看看里面到底发了什么。 深入视图层次结构(View Hierarchy)首先:你要找到 Delete 按钮在视图层次结构里的位置,然后你才能决定是否可以将其用于你自定义的 Cell 。 最容易做到这一点的方式是将 View 的各个部分分别染色,以便清楚地看到它们地位置和范围。 继续在 cell.backgroundColor = [UIColor purpleColor];
cell.contentView.backgroundColor = [UIColor blueColor];
这些颜色足够让我们看清这些视图在 Cell 中的位置。 再次编译并运行,你会看到着色后的元素,如下面的截图所示: 你会清楚地看到蓝色的 往左边拖动 Cell ,你会看到类似下面的的界面: 看起来 Delete 按钮实际上隐藏在 Cell 的下面。唯一能 100% 确保的方式是在视图层次结构中再挖深一点。 为了辅助你的视图考古,你可以用一个只能用于调试的方法,叫做
添加如下打印语句到 #ifdef DEBUG
@"Cell recursive description:nn%@nn",[cell performSelector:@selector(recursiveDescription)]);
#endif
一旦添加了这一行代码,你就会得到一个警告,也就是 编译并运行;你会看到控制台全都是 log 语句,类似下面这样: 2014-02-01 09:56:15.587 SwipeableCell[46989:70b] Cell recursive description:
<UITableViewCell: 0x8e25350; frame = (0 396; 320 44); text = 'Item #10'; autoresize = W; layer = <CALayer: 0x8e254e0>>
| <UITableViewCellScrollView: 0x8e636e0; frame = (0; 44); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x8e1d7d0>; layer = <CALayer: 0x8e1d960>; contentOffset: {0,0}>
| | <UIButton: 0x8e22a70; frame = (302 16; 8 12.5); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x8e22d10>>
| | | <UIImageView: 0x8e20ac0; frame = (12.5); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x8e5efc0>>
| | <UITableViewCellContentView: 0x8e23aa0; frame = (287 44); opaque = NO; gestureRecognizers = <NSArray: 0x8e29c20>; layer = <CALayer: 0x8e62220>>
| | | <UILabel: 0x8e23d70; frame = (15 270 43); text = 'Item #10'; clipsToBounds = NO; layer = <CALayer: 0x8e617d0>>
又要哇~——信息真不少。你所看到的是递归的描述log语句,在每次 Cell 被创建或回收时都会打印。所以你会看到好几个这种消息,因为初始的屏幕上有好几个 Cell 。 虽然信息很多,但它是根据视图层次结构在每个视图上都调用了 为了更加易读,下面光拿出类名和 Frame 来看: <UITableViewCell; frame = (44);> //1
| <UITableViewCellScrollView; frame = (44); > //2
| | <UIButton; frame = (12.5)> //3
| | | <UIImageView; frame = (12.5);> //4
| | <UITableViewCellContentView; frame = (//5
| | | <UILabel; frame = (43);> //6
目前 Cell 里有六个视图:
你会注意到 Delete 按钮并没有显示在上面的视图层次结构排列里。嗯~。可能它只在滑动开始时才被添加到层次结构里。对于优化来说这样做很合理。在不需要 Delete 按钮的时候实在没有必要将其放在那里。要验证这个猜想,就添加如下代码到 ifdef DEBUG
cellForRowAtIndexPath:indexPath] 这和之前添加的一样,除了这次我们需要滑动 Cell 以便调用 tableView:commitEditingStyle:forRowAtIndexPath: :
译者注:上面这一段的原文是“This is the same as before,except this time we need to grab the cell from the table view using cellForRowAtIndexPath:.”,按照我的理解,滑动应该调用 编译并运行;滑动第一个 Cell,并点击 Delete。然后看看控制台的输出,找到最后一个递归描述,即第一个 Cell 的视图层次结构。你知道它是第一个 Cell ,因为它的 <UITableViewCell: 0xa816140; frame = ('Item #1'; autoresize = W; gestureRecognizers = <NSArray: 0x8b635d0>; layer = <CALayer: 0xa816310>>
| <UITableViewCellScrollView: 0xa817070; frame = (NSArray: 0xa8175e0>; layer = <CALayer: 0xa817260>; contentOffset: {82,179)">0}>
| | <UITableViewCellDeleteConfirmationView: 0x8b62d40; frame = (82 44); layer = <CALayer: 0x8b62e20>>
| | | <UITableViewCellDeleteConfirmationButton: 0x8b61b60; frame = (43.5); opaque = NO; autoresize = LM; layer = <CALayer: 0x8b61c90>>
| | | | <UILabel: 0x8b61e60; frame = (11; 52 22); text = 'Delete'; clipsToBounds = YES; userInteractionEnabled = NO; layer = <CALayer: 0x8b61f00>>
| | <UITableViewCellContentView: 0xa816500; frame = (NSArray: 0xa817d40>; layer = <CALayer: 0xa8165b0>>
| | | <UILabel: 0xa8167a0; frame = (43.5); text = 'Item #1'; clipsToBounds = YES; layer = <CALayer: 0xa816840>>
| | <_UITableViewCellSeparatorView: 0x8a2b6e0; frame = (97 43.5; 305 0.5); layer = <CALayer: 0x8a2b790>>
| | <UIButton: 0xa8166a0; frame = (297 NO; layer = <CALayer: 0xa8092b0>>
| | | <UIImageView: 0xa812d50; frame = (NO; layer = <CALayer: 0xa8119c0>>
喔~ 看到 Delete 按钮了!在 Content View 下面, 有一个视图的类名为 现在回到 Cell。 你同样已经学了不少关于这个 Cell 如何工作的知识;亦即,那个 你可以通过在 for (UIView *view in cell.subviews) {
if ([view isKindOfClass:[UIScrollView class]]) {
view.backgroundColor = [UIColor greenColor];
}
}
再次编译并允许应用;绿色高亮确认了这个私有类确实是 回想刚才 但是,这个视图到底有什么用?继续拖动 Cell 到左边,你就会看到 Scroll View 在你拖动 Cell 并 释放时提供了 “弹性(springy)”行为,如下所示: 在你创建你自己的自定义
直白的说,就是,任何对 这就意味着你将找出你自己的解决方案以便添加自定义按钮。但不要害怕,你可以很容易地复制出 Apple 所使用的方案。 可滑动 Table View Cell 的组成列表 这对你来说是什么意思?到了这里,你就有了一个组成列表来制造出一个 我们从 View Stack 的最底部开始列出条目,你的列表如下:
还有一个可能不那么明显的成分:你必须确保系统提供的 好消息是设置默认滑动手势不可用的操作相当简单。 MasterViewController.m修改 BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(return NO;
}
编译并运行;试着滑动某个 Cell ,你会发现你不能再滑动去删除了。 为了保持简单,你将使用两个按钮来走完这个教程。但同样的技术也可以再一个按钮上工作,或者超过两个按钮的情况——作为提醒,你可能需要执行一些本文没有涉及到的调整,如果你真的添加了多个按钮,你必须将整个 Cell 滑出才能看到所有的按钮。 创建一个自定义 Cell 你可以从基本视图和手势识别列表可以看到,在 Table View Cell 中有许多要做的事。你将创建一个自定义的 去往 在 @interface SwipeableCell()
@property (nonatomic,weak) IBOutlet UIButton *button1;
IBOutlet UIButton *button2;
IBOutlet UIView *myContentView;
IBOutlet UILabel *myTextLabel;
@end
下一步,进入 Storyboard 选中 打开 Identity Inspector ,然后修改 Custom Class 为 现在 首先,你要在 Attributes Inspector 里修改两个地方以便自定义视图。设置 Style 为 然后,拖两个按钮到 Cell 的 Content View 里。在视图的 Attributes Inspector 区设置每个按钮的背景色为比较鲜艳的颜色,并设置每个按钮的文字颜色为比较易读的颜色,这样你就可以清楚地看到按钮。 将第一个按钮放在右边,和 接下来,将每个按钮和对应的 Outlet 关联起来。右键单击到可滑动Cell上打开它的 Outlets,然后将 button1 拖动到到右边的按钮, button2 拖动到左边的按钮,如下: 你需要创建一个方法来处理对每个按钮的点击。 SwipeableCell.m添加如下方法: IBAction)buttonClicked:(id)sender {
if (sender == self.button1) {
@"Clicked button 1!");
} else if (sender == self.button2) {
@"Clicked button 2!");
} @"Clicked unknown button!");
}
}
这个方法处理对两个按钮的点击,通过在控制台打印记录,你就能确定按钮被点击了。 再次打开 Storyboard ,将两个按钮都连接上 Action 。右键单击 从事件列表中选择 重复上述步骤,用于第二个按钮。现在随便按照任何一个按钮上,都会调用 SwipeableCell.m添加如下属性: @property (nonatomic,strong) NSString *itemText;
稍后你将更多的和 MasterViewController.m并在顶部添加如下一行: import "SwipeableCell.h"
这将保证这个类知道你自定义的 Cell 子类。 替换 NSIndexPath *)indexPath {
SwipeableCell *cell = [tableView forIndexPath:indexPath];
NSString *item = _objects[indexPath.row];
cell.itemText = item;
现在该使用你的新 Cell 而不是标准的 UITableViewCell 。
编译并运行;你会看到如下界面:
添加一个 Delegate欧耶~ 你的按钮已经出现了!如果你点击任何一个按钮,你都会在控制台看到合适的信息输出。然而,你不能指望 Cell 本身去处理任何直接的 Action 。 比如说,一个 Cell 不能 Present 其他的 View Controller 或直接将其 push 到 Navigation Stack 里。你必须要设置一个 Delegate 来传递按钮的点击事件回到 View Controller 中去处理那个事件。 SwipeableCell.h并在 @protocol SwipeableCellDelegate <NSObject>
- (void)buttonOneActionForItemText:(NSString *)itemText;
- (buttonTwoActionForItemTextNSString *)itemText;
添加如下 Delegate 属性到 SwipeableCell.h ,就在itemText 属性下面:
id <SwipeableCellDelegate> delegate;
更新 if (sender == self.button1) {
[self.delegate buttonOneActionForItemText:self.itemText];
} if (sender == self.button2) {
[buttonTwoActionForItemText: 这个更新使得这个方法去调用合适的 Delegate 方法,而不仅仅是打印一句 log。
现在打开 pragma mark - SwipeableCellDelegate
- (void)buttonOneActionForItemText:(NSString *)itemText {
@"In the delegate,Clicked button one for %@",itemText);
}
- (void)buttonTwoActionForItemText:( 这个方法目前还是简单的打印到控制台,以确保一切传递都工作正常。
接下来,添加如下协议到 MasterViewController () <SwipeableCellDelegate> {
NSMutableArray *_objects;
}
这只是简单地确认这个类会实现 SwipeableCellDelegate 协议。
最后,你要设置这个 View Controller 为 Cell 的 delegate。 添加如下语句到 cell.delegate = self;
编译并运行;当你点击按钮时,你就会看到合适的“In the delegate”消息。 为按钮添加 Action 如果你看到log消息很很高兴了,也可以跳过下一节。然而,如果你喜欢更加实在的东西,你可以添加一些处理,这样当 delegate 方法被调用时,你就可以显示已经引入的 添加如下两个方法到 void)showDetailWithText:(NSString *)detailText
{
//1
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
DetailViewController *detail = [storyboard instantiateViewControllerWithIdentifier:@"DetailViewController"];
detail.title = @"In the delegate!";
detail.detailItem = detailText;
//2
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:detail];
//3
UIBarButtonItem *done = [[UIBarButtonItem initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:closeModal)];
[detail.navigationItem setRightBarButtonItem:done];
[presentViewController:navController animated:YES completion:nil];
}
//4
- (void)closeModal
{
[dismissViewControllerAnimated:nil];
}
在上面的代码里,你执行了四个操作:
接下来,用下列版本替换你之前添加的两个方法: NSString *)itemText
{
[showDetailWithText:[@"Clicked button one for @"Clicked button two for 最后,打开 Main.storyboard 并选中Detail View Controller 。找到 Identity Inspector 并设置Storyboard ID 为DetailViewController 以匹配类名,如下所示:
如果你忘了这一步, 编译并运行;点击某个 Cell 中的按钮,然后看着 Modal View Controller 出现,如下面的截图所示:
添加顶层视图并添加滑动 Action现在你到了视图工作的后段部分,是时候让顶层部分启动并运行起来了。 Main.storyboard并拖一个 如果你要精确地控制,打开 Size Inspector 并设置这个视图地宽和高,分别为 320 和 43: 你同样需要一个约束来将视图钉在 contentView 的边缘。选中视图并点击 连接好这个视图的 Outlet,按照之前介绍的步骤:在左边的导航器里右键单击这个可滑动 Cell 并拖动 下一步,拖动一个 编译并运行;你的 Cell 看起来有正常了: 添加数据 但为何实际的文本数据没有显示出来?那是因为你只是设置了 SwipeableCell.m并添加如下方法: void)setItemText:(NSString *)itemText {
//Update the instance variable
_itemText = itemText;
//Set the text to the custom label.
self.myTextLabel.text = _itemText;
}
这个方法覆写了 最后,为了让接下来的几步的结果更易看到,你将把 item 的 title 变长一点,以便在 Cell 滑动后依然有一些文本可见。 转到 @"Longer Title Item # 编译并运行;你就会看到合适的 item title 显示如下:
手势识别——GO! 终于到了“有趣的”部分——将数学、约束以及手势识别搅和在一起,以方便地处理滑动操作。 首先,在 CGPoint panStartPoint;
@property (nonatomic,179)">CGFloat startingRightLayoutConstraintConstant;
@property (nonatomic,93)">IBOutlet NSLayoutConstraint *contentViewRightConstraint;
@property (nonatomic,179)">NSLayoutConstraint *contentViewLeftConstraint;
关于你所要做的事情,简短版本是这样的:记录一个 Pan 手势并调整你的View的左右约束,根据 a) 用户将 Cell Pan 了多远 b) Cell 在何处以及合适开始移动。 为了做到这一点,你首先要将这个 IBOutlet 连接到 通过打开约束列表,你可以找出这两个约束。通过检查每个约束在 Cell 上的高亮你就能找到那合适的两个。在这个例子中,是 一旦你定位到合适的约束,就将其连接到合适的 Outlet 上——在本例中,是 遵循同样的步骤,连接好 下一步,打开 SwipeableCell() <UIGestureRecognizerDelegate>
然后,依然在 void)awakeFromNib {
[awakeFromNib];
self.panRecognizer = [[UIPanGestureRecognizer initWithTarget:panThisCell:)];
self.panRecognizer.delegate = self;
[self.myContentView addGestureRecognizer:self.panRecognizer];
}
这里设置了 Pan 手势并将其添加到 Cell 上: 再添加如下方法: void)panThisCell:(UIPanGestureRecognizer *)recognizer {
switch (recognizer.state) {
case UIGestureRecognizerStateBegan:
self.panStartPoint = [recognizer translationInView:self.myContentView];
@"Pan Began at panStartPoint));
break;
case UIGestureRecognizerStateChanged: {
CGPoint currentPoint = [recognizer CGFloat deltaX = currentPoint.x - self.panStartPoint.x;
@"Pan Moved %f",deltaX);
}
case UIGestureRecognizerStateEnded:
@"Pan Ended");
case UIGestureRecognizerStateCancelled:
@"Pan Cancelled");
default:
break;
}
}
这个方法会在 Pan 手势识别器发动时执行,暂时,它只简单地打印 Pan 手势的细节。 编译并运行;用手指拖动 Cell ,你就会看到如下log记录了移动信息: 如果你往初始点的右边滑动,你会看到正数,往初始点的左边滑动就会看到负数。这些数字将用于调整 移动这些约束 从本质上将,你需要通过调整将 Cell 的 举例来说,如果 听起来蛮容易的——但还有许多移动相关的事情要注意。根据 Cell 是否已经打开和用户 Pan 的方向,你要处理不同的一大把事情。 你同样需要知道 Cell 最远可以滑动多远。你将通过计算被按钮覆盖的区域的宽度来确定这一点。最简单的方法是用视图的整个宽度减去最左边的按钮的最小 X 位置。 为了阐明,下面来个 sneak peek ,以明确的图示表明你所要关注的方面: 幸好,感谢 添加如下方法到 - (CGFloat)buttonTotalWidth {
CGRectGetWidth(self.frame) - CGRectGetMinX(self.button2.frame);
}
添加如下两个骨架方法到 这两个骨架方法——一旦你填上血肉——将 snap 打开 Cell 并 snap 关闭 Cell。在你对 pan 手势识别起添加更多处理后,你会回到这两个方法。 panThisCell:中的 case UIGestureRecognizerStateBegan:
self.panStartPoint = [recognizer self.myContentView];
self.startingRightLayoutConstraintConstant = self.contentViewRightConstraint.constant;
break;
你需要存储 Cell 的初始位置(例如,约束值)以确定 Cell 是要打开还是关闭。 下一步你需要添加更多处理以应对 pan 手势识别器的改变。还是在 case UIGestureRecognizerStateChanged: {
self.myContentView];
CGFloat deltaX = currentPoint.x - self.panStartPoint.x;
BOOL panningLeft = NO;
if (currentPoint.x < self.panStartPoint.x) { //1
panningLeft = YES;
}
if (self.startingRightLayoutConstraintConstant == 0) { //2
//The cell was closed and is now opening
if (!panningLeft) {
CGFloat constant = MAX(-deltaX,179)">0); //3
if (constant == //4
[resetConstraintContstantsToZero:notifyDelegateDidClose:NO];
} else { //5
self.contentViewRightConstraint.constant = constant;
}
} else {
MIN(-deltaX,[buttonTotalWidth]); //6
if (constant == [buttonTotalWidth]) { //7
[setConstraintsToShowAllButtons:notifyDelegateDidOpen://8
self.contentViewRightConstraint.constant = constant;
}
}
}
上面大部分代码都在 Cell 默认的“关闭”状态下 处理pan手势识别器,下面是细节说明:
哟!处理得真不少… 而这个只是处理了 Cell 已经关闭得情况。你现在还要编写代码处理当手势开始时 Cell 就已经部分开启的情况。 就在刚在添加的代码之下添加如下代码: else {
//The cell was at least partially open.
CGFloat adjustment = self.startingRightLayoutConstraintConstant - deltaX; //1
MAX(adjustment,150)">//2
//3
[//4
self.contentViewRightConstraint.constant = constant;
}
} MIN(adjustment,150)">//5
//6
[//7
self.contentViewRightConstraint.constant = constant;
}
}
}
self.contentViewLeftConstraint.constant = -self.contentViewRightConstraint.constant; //8
}
这是 if 语句的后半段。因此它用于处理 Cell 原本就打开的情况。
再一次,下面说明你要处理的几个情况:
编译并运行;现在你可以来回滑动 Cell !它不是非常流畅,而且它在你希望的地方之前的一点就停下了。这是因为你还没有真正实现那两个用于处理打开和关闭 Cell 的方法。
Snap!接下来,你要让 Cell Snao 进入合适的位置。你会注意到,如果你放手 Cell 会停到合适的位置。 在你进入方法开始处理之前,你需要一个单独的生成动画的方法。 void )updateConstraintsIfNeeded:(BOOL)animated completion:(void (^)(BOOL finished))completion { float duration = 0; if (animated) { duration = 0.1; } [UIView animateWithDuration:duration delay:options:UIViewAnimationOptionCurveEaSEOut animations:^{ [layoutIfNeeded]; } completion:completion]; }
接下来,你将填充那两个处理打开和关闭的骨架方法。记得在 Apple 的原始实现里,因为使用了 要让事情看起来正确,你将在 Cell 撞到边界时给它一点弹性。你同样要确保 添加如下常量到 static CGFloat const kBounceValue = 20.0f;
这个常量存储了弹性值,将用于你的弹性动画中。 如下更新 BOOL)notifyDelegate {
//TODO: Notify delegate.
//1
if (self.startingRightLayoutConstraintConstant == [buttonTotalWidth] &&
self.contentViewRightConstraint.constant == [buttonTotalWidth]) {
return;
}
//2
self.contentViewLeftConstraint.constant = -[buttonTotalWidth] - kBounceValue;
self.contentViewRightConstraint.constant = [buttonTotalWidth] + kBounceValue;
[updateConstraintsIfNeeded:animated completion:^(BOOL finished) {
//3
self.contentViewLeftConstraint.constant = -[buttonTotalWidth];
self.contentViewRightConstraint.constant = [buttonTotalWidth];
[BOOL finished) {
//4
self.startingRightLayoutConstraintConstant = self.contentViewRightConstraint.constant;
}];
}];
}
这个方法在 Cell 完全打开时执行。下面解释发生了什么:
resetConstraintContstantsToZero:notifyDelegateDidClose:: //TODO: Notify delegate.
0 &&
self.contentViewRightConstraint.constant == 0) {
//Already all the way closed,no bounce necessary
return;
}
self.contentViewRightConstraint.constant = -kBounceValue;
self.contentViewLeftConstraint.constant = BOOL finished) {
self.contentViewRightConstraint.constant = 0;
self.contentViewLeftConstraint.constant = 0;
[BOOL finished) {
self.startingRightLayoutConstraintConstant = self.contentViewRightConstraint.constant;
}];
}];
}
如你所见,这类似于 编译并运行;随意滑动 Cell 到它的捕捉点,你就会在放手时看到弹性行为。 然而,如果你在 Cell 完全开启或完全关闭之前将释放手指,它将会卡在中间。Whoops! 你还没有处理触摸结束或被取消的情况。 找到 case UIGestureRecognizerStateEnded:
if (self.startingRightLayoutConstraintConstant == //1
//Cell was opening
CGFloat halfOfButtonOne = CGRectGetWidth(self.button1.frame) / 2; //2
if (self.contentViewRightConstraint.constant >= halfOfButtonOne) { //3
//Open all the way
[YES];
} else {
//Re-close
[YES];
}
} //Cell was closing
CGFloat buttonOnePlusHalfOfButton2 = CGRectGetWidth(self.button1.frame) + (CGRectGetWidth(self.button2.frame) / 2); //4
if (self.contentViewRightConstraint.constant >= buttonOnePlusHalfOfButton2) { //5
//Re-open all the way
[//Close
[YES];
}
}
在这里,你根据 Cell 是否已经打开或关闭以及手势结束时 Cell 的位置在执行不同的处理。具体来讲:
最后,你还要处理一下手势被取消的情况。用如下代码替换 case UIGestureRecognizerStateCancelled:
//Cell was closed - reset everything to 0
[YES];
} //Cell was open - reset to the open state
[YES];
}
这个处理相当直白;由于用户取消了触摸,表示他们不想改变 Cell 当前的状态,所以你只需要将一切都设置为它们原本的样子即可。
编译并运行;滑动 Cell ,你会看到 Cell Snap 到打开或关闭,而不论你的手指再哪里,如下所示: 更好地处理 Table View 在最终完成前,只有少数几步了! 首先,你的 pragma mark - UIGestureRecognizerDelegate - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { YES; }这个方法告知各手势识别器,它们可以同时工作。 编译并运行;打开第一个 Cell 然后你依然可以 Scroll tableView 。 还有一个 Cell 重用引起的小问题:各个行不记得它们的状态,看起来是因为 Cell 重用了它们的视图的 开启/关闭 状态,然后它们的视图就不能正确反应用户的操作了。要查看这一情况,打开一个 Cell ,然后将 Table Scroll 一点点。你就会注意每次都有一个 Cell 始终保持打开状态,但每次都不同。 要修复这个问题头一半,添加如下方法到 这个方法确保 Cell 在其回收重利用时再次关闭。 要解决这个问题的后一半,你将添加一个公共方法给 Cell 以促使其打开。然后你会添加一些 delegate 方法以允许 SwipeableCell.h。在 void)cellDidOpen:(UITableViewCell *)cell;
- (void)cellDidClose:(UITableViewCell *)cell;
这些方法将会通知 delegate —— 在你的情况里,就是 Master View Controller —— 某个 Cell 被打开或关闭了。 添加如下公共方法申明到 void)openCell;
接下来,打开 void)openCell {
[ 这个方法允许 delegate 修改 Cell 的状态。
依然在用一个文件里,找到 if (notifyDelegate) {
[cellDidClose:self];
}
接下来,找到 添加如下属性申明到 NSMutableSet *cellsCurrentlyEditing;
它将存储当前已被打开的 Cell 的列表。 添加如下代码到 self.cellsCurrentlyEditing = [NSMutableSet new];
这个初始化保证了之后你可以正常使用数组。 现在在同一个文件里添加如下方法实现: void)cellDidOpen:(UITableViewCell *)cell {
NSIndexPath *currentEditingIndexPath = [self.tableView indexPathForCell:cell];
[self.cellsCurrentlyEditing addObject:currentEditingIndexPath];
}
- (void)cellDidClose:(UITableViewCell *)cell {
[removeObject:[indexPathForCell:cell]];
}
注意到你添加的时 Index Path 而不是 Cell 本身到列表里。如果你直接添加 Cell 对象,那么之后你就会看到同样的问题,在 Cell 被回收后再次被打开。用了这个方法,你就可以使用合适 的 Index Path 来打开 Cell 了。 最后,添加下面几行到 if ([containsObject:indexPath]) {
[cell openCell];
}
如果当前的 Cell 的 Index Path 在列表里,它就会将其设置为打开。 编译并运行;全都搞定了!你现在有了一个能够 Scroll 的 Table View,还能处理 Cell 的打开和关闭状态,并在 Cell 的任意被点击时,使用 delegate 方法来加载任何任务。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |