美文网首页
iOS之UITableView带滑动操作菜单的Cell(下)

iOS之UITableView带滑动操作菜单的Cell(下)

作者: Obsession丶执 | 来源:发表于2015-12-18 03:25 被阅读1376次

    你同样已经学了不少关于这个 Cell 如何工作的知识;亦即,那个UITableViewCellScrollView
    ,它包含 contentView 和 Disclosure Indicator (以及 Delete 按钮,如果它被添加的话),明显是要做某些事 。你可能已经从它的名字以及它是UIScrollView
    的子类而猜到了。
    你可以通过在tableView:cellForRowAtIndexPath:
    下面添加一个简单的for
    循环来测试这个假设,就在recursiveDescription
    那一行下面:

    for (UIView *view in cell.subviews) { if ([view isKindOfClass:[UIScrollView class]]) { view.backgroundColor = [UIColor greenColor]; }}
    
    

    再次编译并允许应用;绿色高亮确认了这个私有类确实是UIScrollView
    的子类,因为它覆盖了 Cell 里所有的紫色。

    Visible ScrollviewVisible Scrollview
    回想刚才recursiveDescription
    输出的 log,UITableViewCellScrollView
    的 Frame 和 Cell 本身的 Size 是一致的。
    但是,这个视图到底有什么用?继续拖动 Cell 到左边,你就会看到 Scroll View 在你拖动 Cell 并 释放时提供了 “弹性(springy)”行为,如下所示:
    swipeable-demoswipeable-demo
    在你创建你自己的自定义UITableViewCell
    子类之前,还有一件事要注意,它出至 UITableViewCell Class Reference:
    如果你想超越预定义样式,你可以添加子视图到 Cell 的contentView
    上。在添加子视图时,你自己要负责这些视图的位置以及设置它们的内容。

    直白的说,就是,任何对UITableViewCell
    的自定义操作只能在contentView
    中进行。你不能将自己的视图加在 Cell 下面——而必须将它们加在 Cell 的contentView
    上。
    这就意味着你将找出你自己的解决方案以便添加自定义按钮。但不要害怕,你可以很容易地复制出 Apple 所使用的方案。
    可滑动 Table View Cell 的组成列表
    这对你来说是什么意思?到了这里,你就有了一个组成列表来制造出一个UITableViewCell
    子类,以便放上你自定义的按钮。
    我们从 View Stack 的最底部开始列出条目,你的列表如下:
    contentView
    是你的基础视图,因为你只能将子视图添加到它上面。
    在用户滑动后,任何你想显示的UIButon

    一个位于按钮之上的容器视图来装载你所有的内容。
    你可以使用一个UIScrollView
    来作为你的容器视图,就像 Apple 使用的,或者使用一个UIPanGestureRecognizer
    。这同样能够处理滑动去显示/隐藏按钮。你将在项目中采用后一种方案。
    最后,一个装有实际内容的视图。

    还有一个可能不那么明显的成分:你必须确保系统提供的UIPanGestureRecognizer
    —— 它能让你滑动显示 Delete 按钮 —— 不可用。否则系统手势会和自定义手势冲突。
    好消息是设置默认滑动手势不可用的操作相当简单。
    打开MasterViewController.m
    修改tableView:canEditRowAtIndexPath:
    永远返回NO
    ,如下所示:

    - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { return NO;}
    

    编译并运行;试着滑动某个 Cell ,你会发现你不能再滑动去删除了。
    为了保持简单,你将使用两个按钮来走完这个教程。但同样的技术也可以再一个按钮上工作,或者超过两个按钮的情况——作为提醒,你可能需要执行一些本文没有涉及到的调整,如果你真的添加了多个按钮,你必须将整个 Cell 滑出才能看到所有的按钮。
    创建一个自定义 Cell
    你可以从基本视图和手势识别列表可以看到,在 Table View Cell 中有许多要做的事。你将创建一个自定义的UITableViewCell
    子类,以将所有的逻辑放在同一个地方。
    去往File\New\ File…
    并选择iOS\Cocoa Touch\Objective-C class
    ,将新类命名为SwipeableCell
    ,将它设置为UITableViewCell
    的子类 ,如下所示:

    Creating custom cellCreating custom cell
    在SwipeableCell.m
    中设置下列类扩展和IBOutlet
    ,就在#import
    语句后,@implementation
    语句前:
    @interface SwipeableCell()
    @property (nonatomic, weak) IBOutlet UIButton *button1;
    @property (nonatomic, weak) IBOutlet UIButton *button2;
    @property (nonatomic, weak) IBOutlet UIView*myContentView;
    @property (nonatomic, weak) IBOutlet UILabel *myTextLabel;@end
    

    下一步,进入 Storyboard 选中UITableViewCell
    原型,如下所示:

    Select Table View CellSelect Table View Cell
    打开 Identity Inspector ,然后修改 Custom Class 为SwipeableCell
    ,如下所示:
    Change Custom ClassChange Custom Class
    现在UITableViewCell
    原型的名字在左边的 Document Outline 上会显示为 “Swipeable Cell”。右键单击Swipeable Cell – Cell
    ,你会看到一个你之前设置的IBOutlet
    列表:
    New Name and OutletsNew Name and Outlets
    首先,你要在 Attributes Inspector 里修改两个地方以便自定义视图。设置 Style 为Custom
    , Selection 为None
    , Accessory 也为None
    ,截图如下:
    Reset Cell ItemsReset Cell Items
    然后,拖两个按钮到 Cell 的 Content View 里。在视图的 Attributes Inspector 区设置每个按钮的背景色为比较鲜艳的颜色,并设置每个按钮的文字颜色为比较易读的颜色,这样你就可以清楚地看到按钮。
    将第一个按钮放在右边,和contentView
    的上下边缘接触。将第二个按钮放在第一个按钮的左边缘处,也和contentView
    的上下边缘接触。当你做好后,Cell 看起来如下,可能颜色少有差异:
    Buttons Added to Prototype CellButtons Added to Prototype Cell
    接下来,将每个按钮和对应的 Outlet 关联起来。右键单击到可滑动Cell上打开它的 Outlets,然后将 button1 拖动到到右边的按钮, button2 拖动到左边的按钮,如下:
    swipeable-button1swipeable-button1
    你需要创建一个方法来处理对每个按钮的点击。
    打开SwipeableCell.m
    添加如下方法:
    - (IBAction)buttonClicked:(id)sender 
    { 
          if (sender == self.button1) 
          { 
          NSLog(@"Clicked button 1!"); 
          } else if (sender == self.button2)
             { 
              NSLog(@"Clicked button 2!");
             } else { NSLog(@"Clicked unknown button!");
         }
    }
    

    这个方法处理对两个按钮的点击,通过在控制台打印记录,你就能确定按钮被点击了。
    再次打开 Storyboard ,将两个按钮都连接上 Action 。右键单击Swipeable Cell – Cell
    出现 Outlet 和 Action 的列表。从buttonClicked:
    Action 拖动到你的按钮,如下:

    swipeable-buttonClickedswipeable-buttonClicked
    从事件列表中选择Touch Up Inside
    ,如下所示:
    swipeable-touchupinsideswipeable-touchupinside
    重复上述步骤,用于第二个按钮。现在随便按照任何一个按钮上,都会调用buttonClicked:

    打开SwipeableCell.m
    添加如下属性:
    @property (nonatomic, strong) NSString *itemText;
    

    稍后你将更多的和itemText
    打交道,但目前,这就是所有你要做的。
    打开MasterViewController.m
    并在顶部添加如下一行:

    import "SwipeableCell.h"

    这将保证这个类知道你自定义的 Cell 子类。
    替换tableView:cellForRowAtIndexPath:
    的内容为:

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
    {
         SwipeableCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; 
        NSString *item = _objects[indexPath.row]; cell.itemText = item; return cell;
      }
    

    现在该使用你的新 Cell 而不是标准的UITableViewCell

    编译并运行;你会看到如下界面:

    ALL THE BUTTONS!ALL THE BUTTONS!
    添加一个 Delegate
    欧耶~ 你的按钮已经出现了!如果你点击任何一个按钮,你都会在控制台看到合适的信息输出。然而,你不能指望 Cell 本身去处理任何直接的 Action 。
    比如说,一个 Cell 不能 Present 其他的 View Controller 或直接将其 push 到 Navigation Stack 里。你必须要设置一个 Delegate 来传递按钮的点击事件回到 View Controller 中去处理那个事件。
    打开SwipeableCell.h
    并在@interface
    之上添加如下 Delegate 协议:
    @protocol SwipeableCellDelegate <NSObject>
    - (void)buttonOneActionForItemText:(NSString *)itemText;
    - (void)buttonTwoActionForItemText:(NSString *)itemText;
    @end
    

    添加如下 Delegate 属性到SwipeableCell.h
    ,就在itemText
    属性下面:

    @property (nonatomic, weak) id <SwipeableCellDelegate> delegate;
    

    更新SwipeableCell.m
    中的buttonClicked:
    为如下所示:

    - (IBAction)buttonClicked:(id)sender
     { 
          if (sender == self.button1)
           {
               [self.delegate buttonOneActionForItemText:self.itemText]; 
            } else if (sender == self.button2) 
                { 
                      self.delegate buttonTwoActionForItemText:self.itemText];
                 } else { 
                          (@"Clicked unknown button!"); 
                          }
    }
    
    

    这个更新使得这个方法去调用合适的 Delegate 方法,而不仅仅是打印一句 log。
    现在打开MasterViewController.m
    并添加如下 delegate 方法:

    #pragma mark - SwipeableCellDelegate
    - (void)buttonOneActionForItemText:(NSString *)itemText { 
          NSLog(@"In the delegate, Clicked button one for %@", itemText);
        }
    - (void)buttonTwoActionForItemText:(NSString *)itemText {
         NSLog(@"In the delegate, Clicked button two for %@", itemText);
        }
    

    这个方法目前还是简单的打印到控制台,以确保一切传递都工作正常。
    接下来,添加如下协议到MasterViewController.m
    顶部的类扩展上以符合协议申明:

    @interface MasterViewController () <SwipeableCellDelegate>
     { 
           NSMutableArray *_objects;
     }
    @end
    

    这只是简单地确认这个类会实现SwipeableCellDelegate
    协议。
    最后,你要设置这个 View Controller 为 Cell 的 delegate。
    添加如下语句到tableView:cellForRowAtIndexPath:
    ,就在最后的 return 语句之前:
    cell.delegate = self;

    编译并运行;当你点击按钮时,你就会看到合适的“In the delegate”消息。
    为按钮添加 Action
    如果你看到log消息很很高兴了,也可以跳过下一节。然而,如果你喜欢更加实在的东西,你可以添加一些处理,这样当 delegate 方法被调用时,你就可以显示已经引入的DetailViewController

    添加如下两个方法到MasterViewController.m

    - (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 alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(closeModal)];
     [detail.navigationItem setRightBarButtonItem:done]; 
    [self presentViewController:navController animated:YES completion:nil];
    }
    //4
    - (void)closeModal
      { 
            [self dismissViewControllerAnimated:YES completion:nil]; 
      }
    

    在上面的代码里,你执行了四个操作:
    从 Storyboard 里取出 Detail View Controller 并设置其 title 和 detailItem 。
    设置一个UINavigationController
    作为包含 Detail View Controller 的容器,并给你放置 close 按钮的地方。
    添加 close 按钮,关联MasterViewController
    里的一个 Action。
    设置这个 Action 的响应方法,它将 dismiss 任何以 Modal 方式显示 View Controller

    接下来,用下列版本替换你之前添加的两个方法:

    - (void)buttonOneActionForItemText:(NSString *)itemText
    { 
    [self showDetailWithText:[NSString stringWithFormat:@"Clicked button one for %@", itemText]];
    }
    - (void)buttonTwoActionForItemText:(NSString *)itemText
    {
     [self showDetailWithText:[NSString stringWithFormat:@"Clicked button two for %@", itemText]];
    }
    

    最后,打开Main.storyboard
    并选中Detail View Controller
    。找到 Identity Inspector 并设置Storyboard ID
    为DetailViewController
    以匹配类名,如下所示:

    Add Storyboard IdentifierAdd Storyboard Identifier
    如果你忘了这一步,instantiateViewControllerWithIdentifier
    将会因为不合法的参数而 Crash,其异常表示具有这个标识符的 View Controller 并不存在。
    编译并运行;点击某个 Cell 中的按钮,然后看着 Modal View Controller 出现,如下面的截图所示:
    View Launched from DelegateView Launched from Delegate
    添加顶层视图并添加滑动 Action
    现在你到了视图工作的后段部分,是时候让顶层部分启动并运行起来了。
    打开Main.storyboard
    并拖一个UIView
    到SwipeableTableCell
    上,这个视图将占据整个 Cell 的高和宽,并覆盖按钮,所以在Swipe手势能工作之前,你不会再看到它们了。
    如果你要精确地控制,打开 Size Inspector 并设置这个视图地宽和高,分别为 320 和 43:
    swipeable-320-43swipeable-320-43
    你同样需要一个约束来将视图钉在 contentView 的边缘。选中视图并点击Pin
    按钮,选择所有四个间隔约束并设置它们的值为 0 ,如下所示:
    swipeable-constraintswipeable-constraint
    连接好这个视图的 Outlet,按照之前介绍的步骤:在左边的导航器里右键单击这个可滑动 Cell 并拖动myContentView
    到这个新的视图上。
    下一步,拖动一个UILabel
    到视图里;设置其距离左边 20 点,并设置其垂直剧中。再将其连接到myTextLabel
    Outlet 上。
    编译并运行;你的 Cell 看起来有正常了:
    Back to cellsBack to cells
    添加数据
    但为何实际的文本数据没有显示出来?那是因为你只是设置了itemText
    属性,而没有做会影响myTextLabel
    的事情。
    打开SwipeableCell.m
    并添加如下方法:
    - (void)setItemText:(NSString *)itemText 
    { 
    //Update the instance variable _itemText = itemText; 
    //Set the text to the custom label. 
    self.myTextLabel.text = _itemText;}
    

    这个方法覆写了itemText
    属性的 setter 方法。除了更新后面的实例变量,它还会更新可见的 Label。
    最后,为了让接下来的几步的结果更易看到,你将把 item 的 title 变长一点,以便在 Cell 滑动后依然有一些文本可见。
    转到MasterViewController.m
    并更新viewDidLoad
    中的这一行,这是 item title 生成的地方:

    NSString *item = [NSString stringWithFormat:@"Longer Title Item #%d", i];
    

    编译并运行;你就会看到合适的 item title 显示如下:

    Longer Item Titles displayed in custom labelLonger Item Titles displayed in custom label
    手势识别——GO!
    终于到了“有趣的”部分——将数学、约束以及手势识别搅和在一起,以方便地处理滑动操作。
    首先,在SwipeableCell
    的类扩展里添加如下这些属性:
    @property (nonatomic, strong) UIPanGestureRecognizer *panRecognizer;
    @property (nonatomic, assign) CGPoint panStartPoint;
    @property (nonatomic, assign) CGFloat startingRightLayoutConstraintConstant;
    @property (nonatomic, weak) IBOutlet NSLayoutConstraint *contentViewRightConstraint;
    @property (nonatomic, weak) IBOutlet NSLayoutConstraint *contentViewLeftConstraint;
    

    关于你所要做的事情,简短版本是这样的:记录一个 Pan 手势并调整你的View的左右约束,根据 a) 用户将 Cell Pan 了多远 b) Cell 在何处以及合适开始移动。
    为了做到这一点,你首先要将这个 IBOutlet 连接到myContentView
    的左右约束上。这两个约束将视图 钉在 Cell 的contentView
    中。
    通过打开约束列表,你可以找出这两个约束。通过检查每个约束在 Cell 上的高亮你就能找到那合适的两个。在这个例子中,是contentView
    右边和contentView
    之间的约束,如下所示:

    Highlighting ConstraintsHighlighting Constraints
    一旦你定位到合适的约束,就将其连接到合适的 Outlet 上——在本例中,是contentViewRightConstraint
    ,如下图所示:
    Hook Up Constraint to IBOutletHook Up Constraint to IBOutlet
    遵循同样的步骤,连接好contentViewLeftConstraint
    ,它代表contentView
    左边和contentView
    之间的约束。
    下一步,打开SwipeableCell.m
    并修改@interface
    语句的类扩展,添加UIGestureRecognizerDelegate
    协议:
    @interface SwipeableCell() <UIGestureRecognizerDelegate>
    

    然后,依然在SwipeableCell.m
    里,添加如下方法:

    - (void)awakeFromNib
     {
     [super awakeFromNib]; 
    self.panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(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]; 
    NSLog(@"Pan Began at %@", NSStringFromCGPoint(self.panStartPoint)); 
    break; 
    case UIGestureRecognizerStateChanged: { CGPoint currentPoint = [recognizer translationInView:self.myContentView]; 
    CGFloat deltaX = currentPoint.x - self.panStartPoint.x;
     NSLog(@"Pan Moved %f", deltaX); } 
    break; 
    case UIGestureRecognizerStateEnded: NSLog(@"Pan Ended"); 
    break;
     case UIGestureRecognizerStateCancelled: NSLog(@"Pan Cancelled");
     break; 
    default: break;
     }
    }
    

    这个方法会在 Pan 手势识别器发动时执行,暂时,它只简单地打印 Pan 手势的细节。
    编译并运行;用手指拖动 Cell ,你就会看到如下log记录了移动信息:

    Pan LogsPan Logs
    如果你往初始点的右边滑动,你会看到正数,往初始点的左边滑动就会看到负数。这些数字将用于调整myContentView
    的约束。
    移动这些约束
    从本质上将,你需要通过调整将 Cell 的contentView
    钉住的左、右约束来推动myContentView
    到左边。右约束将会接受一个正值,而左约束将接受一个绝对值相等的负值。
    举例来说,如果myContentView
    需要往左移动 5 点,那么 右约束将会接受的值是 5,而左约束将接受的值是 -5 。这将会将整个视图往左边滑动 5 点,而不会改变他的宽度。
    听起来蛮容易的——但还有许多移动相关的事情要注意。根据 Cell 是否已经打开和用户 Pan 的方向,你要处理不同的一大把事情。
    你同样需要知道 Cell 最远可以滑动多远。你将通过计算被按钮覆盖的区域的宽度来确定这一点。最简单的方法是用视图的整个宽度减去最左边的按钮的最小 X 位置。
    为了阐明,下面来个 sneak peek ,以明确的图示表明你所要关注的方面:
    Minimum x of button 2Minimum x of button 2
    幸好,感谢 CGRect
    CGGeometry 函数 ,这些很容易被转换为代码:
    添加如下方法到SwipeableCell.m
    • (CGFloat)buttonTotalWidth { return CGRectGetWidth(self.frame) - CGRectGetMinX(self.button2.frame);}

    添加如下两个骨架方法到SwipeableCell.m

    - (void)resetConstraintContstantsToZero:(BOOL)animated notifyDelegateDidClose:(BOOL)endEditing
    { 
    //TODO: Build.
    }
    - (void)setConstraintsToShowAllButtons:(BOOL)animated notifyDelegateDidOpen:(BOOL)notifyDelegate
    { 
    //TODO: Build
    }
    

    这两个骨架方法——一旦你填上血肉——将 snap 打开 Cell 并 snap 关闭 Cell。在你对 pan 手势识别起添加更多处理后,你会回到这两个方法。
    替换panThisCell:
    中的UIGestureRecognizerStateBegan
    case 为下列代码:

    case UIGestureRecognizerStateBegan: self.panStartPoint = [recognizer translationInView:self.myContentView]; self.startingRightLayoutConstraintConstant = self.contentViewRightConstraint.constant;
     break;
    

    你需要存储 Cell 的初始位置(例如,约束值)以确定 Cell 是要打开还是关闭。
    下一步你需要添加更多处理以应对 pan 手势识别器的改变。还是在panThisCell:
    里,修改UIGestureRecognizerStateChanged
    case ,如下所示:

    case UIGestureRecognizerStateChanged: { CGPoint currentPoint = [recognizer translationInView: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, 0); 
    //3
     if (constant == 0) 
    {
     //4 [self resetConstraintContstantsToZero:YES notifyDelegateDidClose:NO]; 
    } else { 
    //5 self.contentViewRightConstraint.constant = constant;
     } 
    } else {
     CGFloat constant = MIN(-deltaX, [self buttonTotalWidth]); 
    //6 if (constant == [self buttonTotalWidth]) { 
    //7 [self setConstraintsToShowAllButtons:YES notifyDelegateDidOpen:NO];
     } else { 
    //8 self.contentViewRightConstraint.constant = constant; 
    }
     }
     }
    

    上面大部分代码都在 Cell 默认的“关闭”状态下 处理pan手势识别器,下面是细节说明:
    判断 pan 手势是往左还是往右。
    如果右约束常量为 0 ,意味着myContentView
    完全挡住contentView
    。因此 Cell 在这里一定已经关闭,而用户准备打开它。
    这是处理用户从做到右滑动以关闭 Cell 的 情况。除了说“你不能做那个”之外,你还要处理的情况是,当用户滑动 Cell 只打开一点点,然后他们希望不必抬起他们的手指来结束此手势就可以滑动它关闭。译者注:就是说,打开一点点不会完全显示出后面的按钮,Cell 会自动关闭。
    因为一个从左到右的滑动会导致deltaX
    为正值,而从右到左的滑动回到导致deltaX
    为负值,你必须根据负的deltaX
    计算出常量以设置到右约束上。因为是从它与0中找出最大值,所以视图不可能往右边走多远。

    如果常量为 0,Cell 就是完全关闭的。调用处理关闭的方法——它(如你回忆起的)在目前还什么也不会做。
    如果常量为不为 0,那么你就将其设置到右手边的约束上。
    否者,如果是从右往做滑动,那么用户试图打开 Cell 。这在个情况里,常量将会小于负deltaX
    或两个按钮的宽度之和。
    如果目标常量是两个按钮的宽度之和,那么 Cell 就被打开至捕捉点(catch point),你应该调用方法来处理这个打开状态。
    如果常量不是两个按钮的宽度之和,那就将其设置到右约束上。

    哟!处理得真不少… 而这个只是处理了 Cell 已经关闭得情况。你现在还要编写代码处理当手势开始时 Cell 就已经部分开启的情况。
    就在刚在添加的代码之下添加如下代码:

    else { 
    //The cell was at least partially open. CGFloat adjustment = self.startingRightLayoutConstraintConstant - deltaX; 
    //1 if (!panningLeft) { CGFloat constant = MAX(adjustment, 0); 
    //2 if (constant == 0) { 
    //3 [self resetConstraintContstantsToZero:YES notifyDelegateDidClose:NO];
     } else { 
    //4 self.contentViewRightConstraint.constant = constant; 
    } 
    } else { 
    CGFloat constant = MIN(adjustment, [self buttonTotalWidth]); 
    //5 if (constant == [self buttonTotalWidth]) { 
    //6 [self setConstraintsToShowAllButtons:YES notifyDelegateDidOpen:NO]; 
    } else {
     //7 self.contentViewRightConstraint.constant = constant; 
    }
     }
     } self.contentViewLeftConstraint.constant = -self.contentViewRightConstraint.constant; //8
    } break;
    
    这是 if 语句的后半段。因此它用于处理 Cell 原本就打开的情况。
    再一次,下面说明你要处理的几个情况:
    在这个情况下,你只是接受deltaX
    ,你就用 rightLayoutConstraint 的原始位置减去deltaX
    以便得知要做多少调整。
    如果用户从做往右滑动,你必须接受 adjustment 与 0 中的较大值。如果 adjustment 已变成负值,那就说明用户已经把 Cell 滑到边界之外了,Cell 就关闭了,这就让你进入下一个情况。
    如果常量为 0,那么 Cell 已经关闭,你就调用处理其关闭的方法。
    否则,将常量设置到右约束上。
    对于从右到左的滑动,你将接受 adjustment 与 两个按钮宽度之和 中的较小值。如果 adjustment 更大,那就表示用户已经滑出超过捕捉点了。
    如果常量刚好等于两个按钮宽度之和,那么 Cell 就打开了,你必须调用处理 Cell 打开的方法。
    否则,将常量设置到右约束上。
    现在,你已经处理完“Cell关闭”和“Cell部分开启”的情况,在这两个情况里,你都可对左约束做同样的事情:将其设置为右约束常量的负值。这就保证了myContentView
    的宽度一直保持不变。
    
    编译并运行;现在你可以来回滑动 Cell !它不是非常流畅,而且它在你希望的地方之前的一点就停下了。这是因为你还没有真正实现那两个用于处理打开和关闭 Cell 的方法。
    Note:你可以也注意到,Table View 本身已经不会 scroll 了。不要担心,一旦你正确处理好 Cell 的滑动,你就能修复它。
    
    Snap!
    接下来,你要让 Cell Snao 进入合适的位置。你会注意到,如果你放手 Cell 会停到合适的位置。
    在你进入方法开始处理之前,你需要一个单独的生成动画的方法。
    打开SwipeableCell.m
    并添加如下方法:
    ```objc
    - (void)updateConstraintsIfNeeded:(BOOL)animated completion:(void (^)(BOOL finished))completion {
     float duration = 0;
     if (animated)
     { 
    duration = 0.1; 
    } 
    [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ 
    [self layoutIfNeeded];
     } completion:completion];
    }
    

    Note:0.1 秒的间隔和 ease-out curve 动画都是我从实践和错误中总结出来的。如果你找到其他更让你看着愉悦的速度或动画类型,可以自由修改它们。

    接下来,你将填充那两个处理打开和关闭的骨架方法。记得在 Apple 的原始实现里,因为使用了UIScrollView
    子类作为最底层的试图,所以会有一点弹性。
    要让事情看起来正确,你将在 Cell 撞到边界时给它一点弹性。你同样要确保contentView
    和myContentView
    有同样的backgroundColor
    以造成弹性非常顺滑的错觉。
    添加如下常量到SwipeableCell.m
    顶部,就在 import 语句之下:
    static CGFloat const kBounceValue = 20.0f;

    这个常量存储了弹性值,将用于你的弹性动画中。
    如下更新setConstraintsToShowAllButtons:notifyDelegateDidOpen:

    - (void)setConstraintsToShowAllButtons:(BOOL)animated notifyDelegateDidOpen:(BOOL)notifyDelegate
     { 
    //TODO: Notify delegate. 
    //1 if (self.startingRightLayoutConstraintConstant == [self buttonTotalWidth] && self.contentViewRightConstraint.constant == [self buttonTotalWidth])
     { 
    return; 
    } 
    //2 self.contentViewLeftConstraint.constant = -[self buttonTotalWidth] - kBounceValue; 
    self.contentViewRightConstraint.constant = [self buttonTotalWidth] + kBounceValue; 
    [self updateConstraintsIfNeeded:animated completion:^(BOOL finished)
     { 
    //3 self.contentViewLeftConstraint.constant = -[self buttonTotalWidth]; 
    self.contentViewRightConstraint.constant = [self buttonTotalWidth];
     [self updateConstraintsIfNeeded:animated completion:^(BOOL finished) 
    { 
    //4 self.startingRightLayoutConstraintConstant = self.contentViewRightConstraint.constant; 
    }];
     }];
    }
    

    这个方法在 Cell 完全打开时执行。下面解释发生了什么:
    如果 Cell 已经开启,约束已经到达完全开启值,那就返回——否则弹性操作将会一次又一次的发生,就像你继续滑动超过总按钮宽度那样。
    你初始设置约束值为按钮总宽度和弹性值的结合值,它将 Cell 拉到左边一点点,这样才好 snap 回来。然后你就调用动画来实现这个设置。
    当第一个动画完成,发动第二个动画,它将 Cell 正好打开在从按钮宽度的位置。
    当第二个动画完成,重设起始约束否则你会看到多次弹跳。

    如下更新resetConstraintContstantsToZero:notifyDelegateDidClose:

    - (void)resetConstraintContstantsToZero:(BOOL)animated notifyDelegateDidClose:(BOOL)notifyDelegate { 
    //TODO: Notify delegate. if (self.startingRightLayoutConstraintConstant == 0 && self.contentViewRightConstraint.constant == 0) { 
    //Already all the way closed, no bounce necessary return;
     }
     self.contentViewRightConstraint.constant = -kBounceValue; self.contentViewLeftConstraint.constant = kBounceValue; 
    [self updateConstraintsIfNeeded:animated completion:^(BOOL finished) { self.contentViewRightConstraint.constant = 0; self.contentViewLeftConstraint.constant = 0;
     [self updateConstraintsIfNeeded:animated completion:^(BOOL finished) { self.startingRightLayoutConstraintConstant = self.contentViewRightConstraint.constant; 
    }];
     }];
    }
    

    如你所见,这类似于setConstraintsToShowAllButtons:notifyDelegateDidOpen:
    ,但它的逻辑是关闭 Cell 而不是打开。
    编译并运行;随意滑动 Cell 到它的捕捉点,你就会在放手时看到弹性行为。
    然而,如果你在 Cell 完全开启或完全关闭之前将释放手指,它将会卡在中间。Whoops! 你还没有处理触摸结束或被取消的情况。
    找到panThisCell:
    用下列代码替换UIGestureRecognizerStateEnded

    case :
    case UIGestureRecognizerStateEnded: 
    if (self.startingRightLayoutConstraintConstant == 0) { 
    //1 //Cell was opening 
    CGFloat halfOfButtonOne = CGRectGetWidth(self.button1.frame) / 2; 
    //2 
    if (self.contentViewRightConstraint.constant >= halfOfButtonOne) { 
    //3
     //Open all the way
     [self setConstraintsToShowAllButtons:YES notifyDelegateDidOpen:YES];
     } else {
    //Re-close [self resetConstraintContstantsToZero:YES notifyDelegateDidClose:YES]; 
    } 
    } else { 
    //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 
    [self setConstraintsToShowAllButtons:YES notifyDelegateDidOpen:YES];
     } else { 
    //Close
     [self resetConstraintContstantsToZero:YES notifyDelegateDidClose:YES]; 
    } 
    } 
    break;
    

    在这里,你根据 Cell 是否已经打开或关闭以及手势结束时 Cell 的位置在执行不同的处理。具体来讲:
    通过检查开始右约束值,得知手势开始时 Cell 是否已经打开或关闭。
    如果 Cell 是关闭的,那你就正在打开它,你要让 Cell 自动滑动到打开,至少需要先滑动右边按钮(self.button1)一半的宽度。因为你在测量约束的常量,你只需要计算实际的按钮宽度,而不是它在视图中的 X 位置。
    接下来,测试约束是否已被打开至超过你希望让 Cell 自动打开的点。如果已经超过,那就自动打开 Cell。如果没有,那就自动关闭 Cell。
    此处表示 Cell 从打开的状态开始,你需要那个能让 Cell 自动 snap 关闭的点,至少需要超过最左边按钮的一半。 将不是最左边的按钮的那些按钮的宽度加起来,在这个情况里,只有 self.button1 而已,再加上最左边按钮的一半——也就是 self.button2 —— 以便找到需要的检查点。
    测试约束是否以及超过这个点,即你希望 Cell 自动关闭的那个点。如果超过了,关闭 Cell。如果没有,那就重新打开 Cell。

    最后,你还要处理一下手势被取消的情况。用如下代码替换UIGestureRecognizerStateCancelled

    case :
    case UIGestureRecognizerStateCancelled:
     if (self.startingRightLayoutConstraintConstant == 0) {
     //Cell was closed - reset everything to 0 
    [self resetConstraintContstantsToZero:YES notifyDelegateDidClose:YES]; 
    } else { 
    //Cell was open - reset to the open state
     [self setConstraintsToShowAllButtons:YES notifyDelegateDidOpen:YES];
     } 
    break;
    

    这个处理相当直白;由于用户取消了触摸,表示他们不想改变 Cell 当前的状态,所以你只需要将一切都设置为它们原本的样子即可。
    编译并运行;滑动 Cell ,你会看到 Cell Snap 到打开或关闭,而不论你的手指再哪里,如下所示:

    swipeable-bounceswipeable-bounce
    更好地处理 Table View
    在最终完成前,只有少数几步了!
    首先,你的UIPanGestureRecognizer
    有时候会影响UITableView
    的 Scroll 操作。由于你已经设置了 Cell 的 Pan 手势识别器 的UIGestureRecognizerDelegate
    ,你只需要实现一个(有些滑稽且冗长命名的) delegate 方法即可将一切恢复正常。
    添加如下方法到SwipeableCell.m
    #pragma mark - UIGestureRecognizerDelegate
    - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
    { 
    return YES;
    }
    

    这个方法告知各手势识别器,它们可以同时工作。
    编译并运行;打开第一个 Cell 然后你依然可以 Scroll tableView 。
    还有一个 Cell 重用引起的小问题:各个行不记得它们的状态,看起来是因为 Cell 重用了它们的视图的 开启/关闭 状态,然后它们的视图就不能正确反应用户的操作了。要查看这一情况,打开一个 Cell ,然后将 Table Scroll 一点点。你就会注意每次都有一个 Cell 始终保持打开状态,但每次都不同。
    要修复这个问题头一半,添加如下方法到SwipeableCell.m

    - (void)prepareForReuse {
     [super prepareForReuse]; 
    [self resetConstraintContstantsToZero:NO notifyDelegateDidClose:NO];
    }
    

    这个方法确保 Cell 在其回收重利用时再次关闭。
    要解决这个问题的后一半,你将添加一个公共方法给 Cell 以促使其打开。然后你会添加一些 delegate 方法以允许MasterViewController
    去管理那个 Cell 是打开的。
    打开SwipeableCell.h
    。在SwipeableCellDelegate
    协议的申明里,添加如下两个新的方法,就在已存在的那两个下面:

    - (void)cellDidOpen:(UITableViewCell *)cell;
    - (void)cellDidClose:(UITableViewCell *)cell;
    

    这些方法将会通知 delegate —— 在你的情况里,就是 Master View Controller —— 某个 Cell 被打开或关闭了。
    添加如下公共方法申明到SwipeableCell
    的@interface
    里:

    - (void)openCell;
    

    接下来,打开SwipeableCell.m
    并添加openCell
    的实现:

    - (void)openCell { 
    [self setConstraintsToShowAllButtons:NO notifyDelegateDidOpen:NO];
    }
    

    这个方法允许 delegate 修改 Cell 的状态。
    依然在用一个文件里,找到resetConstraintsToZero:notifyDelegateDidOpen:
    并替换其中TODO
    为如下代码:

    if (notifyDelegate) {
     [self.delegate cellDidClose:self];
    }
    

    接下来,找到setConstraintsToShowAllButtons:notifyDelegateDidClose:
    并替换其中TODO
    为如下代码:

    if (notifyDelegate) { [self.delegate cellDidOpen:self];}
    

    这两个修改会在一个 swipe 手势完成时通知 delegate ,无论 Cell 是否以及打开或关闭。
    添加如下属性申明到MasterViewController.m
    顶部的类扩展里:

    @property (nonatomic, strong) NSMutableSet *cellsCurrentlyEditing;
    

    它将存储当前已被打开的 Cell 的列表。
    添加如下代码到viewDidLoad
    的最后:

    self.cellsCurrentlyEditing = [NSMutableSet new];
    

    这个初始化保证了之后你可以正常使用数组。
    现在在同一个文件里添加如下方法实现:

    - (void)cellDidOpen:(UITableViewCell *)cell {
     NSIndexPath *currentEditingIndexPath = [self.tableView indexPathForCell:cell];
     [self.cellsCurrentlyEditing addObject:currentEditingIndexPath];
    }
    - (void)cellDidClose:(UITableViewCell *)cell { 
    [self.cellsCurrentlyEditing removeObject:[self.tableView indexPathForCell:cell]];
    }
    

    注意到你添加的时 Index Path 而不是 Cell 本身到列表里。如果你直接添加 Cell 对象,那么之后你就会看到同样的问题,在 Cell 被回收后再次被打开。用了这个方法,你就可以使用合适 的 Index Path 来打开 Cell 了。
    最后,添加下面几行到tableView:cellForRowAtIndexPath:
    ,就在 return 语句之前:

    if ([self.cellsCurrentlyEditing containsObject:indexPath]) { [cell openCell];}
    

    如果当前的 Cell 的 Index Path 在列表里,它就会将其设置为打开。
    编译并运行;全都搞定了!你现在有了一个能够 Scroll 的 Table View,还能处理 Cell 的打开和关闭状态,并在 Cell 的任意被点击时,使用 delegate 方法来加载任何任务。
    下一步怎么走?
    译者注:吐血,终于翻译到这一句了!
    最终的项目可以在此处下载。我还会继续我在此所开发的东西,并组成一个开源项目,以便让事情更有灵活性——在准备好推出时,我会在论坛里贴个链接。
    任何时候,如你在不知道他们如何做到的情况下复制出 Apple 所做的某些效果,你都会发现有许多许多的方式去做到这样的效果。所以这里的方案只是这个效果的实现办法之一;然而,它是我所发现的唯一一个不需要处理嵌套 Scroll View 的办法,产生的手势识别冲突也可以非常简单地解决! :]
    写这篇文章时有一些很有用的资源,但文章里最终使用了非常不同的办法。这些资源是 Ash Furrow 的文章 能让一切都工作起来,以及 Massimiliano Bigatti’s BMXSwipeableCell 项目,它现实通过UIScrollView
    这条路可以挖到多深。
    如果你有任何建议、问题或相关的代码,请在评论区讲出來吧!
    译者:@nixzhu
    转载自:
    https://github.com/nixzhu/dev-blog

    相关文章

      网友评论

          本文标题:iOS之UITableView带滑动操作菜单的Cell(下)

          本文链接:https://www.haomeiwen.com/subject/zgyphttx.html