在iOS 7的邮件应用中,有一种名为“滑动删除”和“更多”按钮的功能。

246
如何在表视图中实现“更多”按钮(就像iOS 7中的邮件应用程序一样),当用户滑动单元格时?
我一直在这里和Cocoa Touch论坛上寻找这些信息,但似乎找不到答案,希望有比我更聪明的人能给我一个解决方案。
当用户滑动表视图单元格时,我想显示不止一个编辑按钮(默认是删除按钮)。在iOS 7的邮件应用程序中,您可以向左滑动以删除,但会出现一个“更多”按钮。 enter image description here

3
看起来,这个苹果开发者论坛的帖子(https://devforums.apple.com/message/860459#860459)中有人已经自己构建了实现。您可以在GitHub上找到一个能够做到您想要的功能的项目:https://github.com/daria-kopaliani/DAContextMenuTableViewController - Guy Kahlon
8
@GuyKahlonMatrix,谢谢你提供的解决方案,它非常有效。这个问题在很多谷歌搜索结果中排名第一,人们不得不通过评论来交换知识,因为某些人认为关闭问题并传播民主思想更有帮助。显然,这个地方需要更好的管理者。 - Şafak Gezer
请查看此链接:http://www.teehanlax.com/blog/reproducing-the-ios-7-mail-apps-interface/,以及我使用自动布局实现的代码:https://github.com/Jafared/JASwipeCellExperiment。 - Alexis
2
如果你的目标是iOS 8,那么下面的答案就是你想要的。 - Johnny
显示剩余4条评论
20个回答

4

Swift 4和iOS 11+

@available(iOS 11.0, *)
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {

    let delete = UIContextualAction(style: .destructive, title: "Delete") { _, _, handler in

        handler(true)
        // handle deletion here
    }

    let more = UIContextualAction(style: .normal, title: "More") { _, _, handler in

        handler(true)
        // handle more here
    }

    return UISwipeActionsConfiguration(actions: [delete, more])
}

4

Swift 3实际答案

这是你唯一需要的函数。对于自定义操作,您不需要CanEdit或CommitEditingStyle函数。

func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
    let action1 = UITableViewRowAction(style: .default, title: "Action1", handler: {
        (action, indexPath) in
        print("Action1")
    })
    action1.backgroundColor = UIColor.lightGray
    let action2 = UITableViewRowAction(style: .default, title: "Action2", handler: {
        (action, indexPath) in
        print("Action2")
    })
    return [action1, action2]
}

4

对于Swift编程

func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
  if editingStyle == UITableViewCellEditingStyle.Delete {
    deleteModelAt(indexPath.row)
    self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
  }
  else if editingStyle == UITableViewCellEditingStyle.Insert {
    println("insert editing action")
  }
}

func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [AnyObject]? {
  var archiveAction = UITableViewRowAction(style: .Default, title: "Archive",handler: { (action: UITableViewRowAction!, indexPath: NSIndexPath!) in
        // maybe show an action sheet with more options
        self.tableView.setEditing(false, animated: false)
      }
  )
  archiveAction.backgroundColor = UIColor.lightGrayColor()

  var deleteAction = UITableViewRowAction(style: .Normal, title: "Delete",
      handler: { (action: UITableViewRowAction!, indexPath: NSIndexPath!) in
        self.deleteModelAt(indexPath.row)
        self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic);
      }
  );
  deleteAction.backgroundColor = UIColor.redColor()

  return [deleteAction, archiveAction]
}

func deleteModelAt(index: Int) {
  //... delete logic for model
}

@bibscy 欢迎提出修改建议。我已经很久没有使用Swift了,所以不确定正确的语法是什么。 - Michael Yagudaev

3

我想给我的应用程序添加相同的功能,在经过多次查阅教程(raywenderlich 是最好的自行解决方案),我发现苹果有其自己的 UITableViewRowAction 类,非常方便。

你需要将TableView的常规方法更改为如下所示:

override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [AnyObject]?  {
    // 1   
    var shareAction = UITableViewRowAction(style: UITableViewRowActionStyle.Default, title: "Share" , handler: { (action:UITableViewRowAction!, indexPath:NSIndexPath!) -> Void in
    // 2
    let shareMenu = UIAlertController(title: nil, message: "Share using", preferredStyle: .ActionSheet)

    let twitterAction = UIAlertAction(title: "Twitter", style: UIAlertActionStyle.Default, handler: nil)
    let cancelAction = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel, handler: nil)

    shareMenu.addAction(twitterAction)
    shareMenu.addAction(cancelAction)


    self.presentViewController(shareMenu, animated: true, completion: nil)
    })
    // 3
    var rateAction = UITableViewRowAction(style: UITableViewRowActionStyle.Default, title: "Rate" , handler: { (action:UITableViewRowAction!, indexPath:NSIndexPath!) -> Void in
    // 4
    let rateMenu = UIAlertController(title: nil, message: "Rate this App", preferredStyle: .ActionSheet)

    let appRateAction = UIAlertAction(title: "Rate", style: UIAlertActionStyle.Default, handler: nil)
    let cancelAction = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel, handler: nil)

    rateMenu.addAction(appRateAction)
    rateMenu.addAction(cancelAction)


    self.presentViewController(rateMenu, animated: true, completion: nil)
    })
    // 5
    return [shareAction,rateAction]
  }

您可以在此网站上了解更多信息。苹果公司的官方文档对于更改背景颜色非常有用:
块引用:

操作按钮的背景颜色。

声明 OBJECTIVE-C @property(nonatomic, copy) UIColor *backgroundColor 讨论 使用此属性指定按钮的背景颜色。如果您未为此属性指定值,则UIKit基于样式属性中的值分配默认颜色。

可用性 适用于iOS 8.0及更高版本。

如果您想更改按钮的字体,这会有点棘手。我在SO上看到过另一篇文章。为了提供代码以及链接,这是他们在那里使用的代码。您必须更改按钮的外观。您必须特定引用tableviewcell,否则您将更改整个应用程序中的按钮外观(我不希望那样,但可能您希望,我不知道:))
Objective C:
+ (void)setupDeleteRowActionStyleForUserCell {

    UIFont *font = [UIFont fontWithName:@"AvenirNext-Regular" size:19];

    NSDictionary *attributes = @{NSFontAttributeName: font,
                      NSForegroundColorAttributeName: [UIColor whiteColor]};

    NSAttributedString *attributedTitle = [[NSAttributedString alloc] initWithString: @"DELETE"
                                                                          attributes: attributes];

    /*
     * We include UIView in the containment hierarchy because there is another button in UserCell that is a direct descendant of UserCell that we don't want this to affect.
     */
    [[UIButton appearanceWhenContainedIn:[UIView class], [UserCell class], nil] setAttributedTitle: attributedTitle
                                                                                          forState: UIControlStateNormal];
}

Swift:

    //create your attributes however you want to
    let attributes = [NSFontAttributeName: UIFont.systemFontOfSize(UIFont.systemFontSize())] as Dictionary!            

   //Add more view controller types in the []
    UIButton.appearanceWhenContainedInInstancesOfClasses([ViewController.self])

这是我认为最简单、最流畅的版本。希望能对你有所帮助。 更新:这是 Swift 3.0 版本:
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
    var shareAction:UITableViewRowAction = UITableViewRowAction(style: .default, title: "Share", handler: {(action, cellIndexpath) -> Void in
        let shareMenu = UIAlertController(title: nil, message: "Share using", preferredStyle: .actionSheet)

        let twitterAction = UIAlertAction(title: "Twitter", style: .default, handler: nil)
        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)

        shareMenu.addAction(twitterAction)
        shareMenu.addAction(cancelAction)


        self.present(shareMenu,animated: true, completion: nil)
    })

    var rateAction:UITableViewRowAction = UITableViewRowAction(style: .default, title: "Rate" , handler: {(action, cellIndexpath) -> Void in
        // 4
        let rateMenu = UIAlertController(title: nil, message: "Rate this App", preferredStyle: .actionSheet)

        let appRateAction = UIAlertAction(title: "Rate", style: .default, handler: nil)
        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)

        rateMenu.addAction(appRateAction)
        rateMenu.addAction(cancelAction)


        self.present(rateMenu, animated: true, completion: nil)
    })
    // 5
    return [shareAction,rateAction]
}

1
谢谢您的回答,我相信它会帮助许多开发人员。 是的,您是正确的,实际上从 iOS 8 开始 Apple 就提供了这个解决方案。但不幸的是,这个本地解决方案并未提供完整的功能。例如,在 Apple 的 Mail 应用程序中,您可以从两侧(左侧一个按钮,右侧三个按钮)获得按钮,但使用当前的 Apple API 您无法在两侧添加按钮,并且当前的 API 不支持用户长时间向每一侧滑动时的默认操作。 目前我认为最好的解决方案是开源的 MGSwipeTableCell。 - Guy Kahlon
@GuyKahlon,你在左右滑动问题方面是完全正确的,我同意如果需要更多自定义,MGSwipeTableCell是最好的选择。苹果自带的选项不是最复杂的,但对于简单任务,我发现它最为直观。 - Septronic
@Septronic 你能否将你的代码更新到Swift 3吗?shareMenu.不再使用addAction方法。谢谢。 - bibscy
@bibscy,我已经添加了Swift版本。你需要属性的位吗?sharemenu只是一个UIAlertController,所以它应该执行操作。试一下,如果有运气的话,请告诉我 :) - Septronic

3
这可能会对你有所帮助:
-(NSArray *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewRowAction *button = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDefault title:@"Button 1" handler:^(UITableViewRowAction *action, NSIndexPath *indexPath)
    {
        NSLog(@"Action to perform with Button 1");
    }];
    button.backgroundColor = [UIColor greenColor]; //arbitrary color
    UITableViewRowAction *button2 = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDefault title:@"Button 2" handler:^(UITableViewRowAction *action, NSIndexPath *indexPath)
                                    {
                                        NSLog(@"Action to perform with Button2!");
                                    }];
    button2.backgroundColor = [UIColor blueColor]; //arbitrary color

    return @[button, button2]; //array with all the buttons you want. 1,2,3, etc...
}

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    // you need to implement this method too or nothing will work:
}

- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
    return YES; //tableview must be editable or nothing will work...
}

2
我使用了 tableViewCell 来展示多个数据,当在单元格上从右向左滑动时,会显示两个按钮 Approve 和 Reject。有两种方法可供选择,第一种是 ApproveFunc,它需要一个参数,另一种是 RejectFunc,它也需要一个参数。

enter image description here

func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
        let Approve = UITableViewRowAction(style: .normal, title: "Approve") { action, index in

            self.ApproveFunc(indexPath: indexPath)
        }
        Approve.backgroundColor = .green

        let Reject = UITableViewRowAction(style: .normal, title: "Reject") { action, index in

            self.rejectFunc(indexPath: indexPath)
        }
        Reject.backgroundColor = .red



        return [Reject, Approve]
    }

    func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
    }

    func ApproveFunc(indexPath: IndexPath) {
        print(indexPath.row)
    }
    func rejectFunc(indexPath: IndexPath) {
        print(indexPath.row)
    }

你能否在你的回答中添加一些解释,以便读者可以从中学习? - Nico Haase
感谢您提供这段代码片段,它可能会提供一些有限的、即时的帮助。通过展示为什么这是一个好的解决方案,一个适当的解释将极大地提高其长期价值,并使其对未来读者具有其他类似问题更有用。请[编辑]您的答案以添加一些解释,包括您所做出的假设。 - Tim Diekmann

2

Swift 4

func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
    let delete = UIContextualAction(style: .destructive, title: "Delete") { (action, sourceView, completionHandler) in
        print("index path of delete: \(indexPath)")
        completionHandler(true)
    }
    let rename = UIContextualAction(style: .normal, title: "Edit") { (action, sourceView, completionHandler) in
        print("index path of edit: \(indexPath)")
        completionHandler(true)
    }
    let swipeActionConfig = UISwipeActionsConfiguration(actions: [rename, delete])
    swipeActionConfig.performsFirstActionWithFullSwipe = false
    return swipeActionConfig
}

你的代码中的源视图是什么?它是图标还是图片? - Saeed Rahmatolahi
1
@SaeedRahmatolahi,“sourceView”是“显示操作的视图”。有关更多信息,请搜索“UIContextualAction.Handler”。 - Mark Moeykens

1
这是一种比较脆弱的方法,不涉及私有API或构建自己的系统。在使用中,需要考虑苹果不会破坏它,并希望他们发布一个API来替换这几行代码。
  1. KVO self.contentView.superview.layer.sublayer. 在init中执行。这是UIScrollView的层。您无法KVO“subviews”。
  2. 当子视图更改时,在scrollview.subviews中查找删除确认视图。 这是在观察回调中完成的。
  3. 将该视图的大小加倍,并在其唯一的子视图左侧添加一个UIButton。 这也是在观察回调中完成的。删除确认视图的唯一子视图是删除按钮。
  4. (可选)UIButton事件应向上查找self.superview,直到找到UITableView,然后调用您创建的数据源或委托方法,例如tableView:commitCustomEditingStyle:forRowAtIndexPath:。您可以使用[tableView indexPathForCell:self]来查找单元格的indexPath。

这还要求您实现标准的表视图编辑委托回调。

static char kObserveContext = 0;

@implementation KZTableViewCell {
    UIScrollView *_contentScrollView;
    UIView *_confirmationView;
    UIButton *_editButton;
    UIButton *_deleteButton;
}

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        _contentScrollView = (id)self.contentView.superview;

        [_contentScrollView.layer addObserver:self
             forKeyPath:@"sublayers"
                options:0
                context:&kObserveContext];

        _editButton = [UIButton new];
        _editButton.backgroundColor = [UIColor lightGrayColor];
        [_editButton setTitle:@"Edit" forState:UIControlStateNormal];
        [_editButton addTarget:self
                        action:@selector(_editTap)
              forControlEvents:UIControlEventTouchUpInside];

    }
    return self;
}

-(void)dealloc {
    [_contentScrollView.layer removeObserver:self forKeyPath:@"sublayers" context:&kObserveContext];
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if(context != &kObserveContext) {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
        return;
    }
    if(object == _contentScrollView.layer) {
        for(UIView * view in _contentScrollView.subviews) {
            if([NSStringFromClass(view.class) hasSuffix:@"ConfirmationView"]) {
                _confirmationView = view;
                _deleteButton = [view.subviews objectAtIndex:0];
                CGRect frame = _confirmationView.frame;
                CGRect frame2 = frame;
                frame.origin.x -= frame.size.width;
                frame.size.width *= 2;
                _confirmationView.frame = frame;

                frame2.origin = CGPointZero;
                _editButton.frame = frame2;
                frame2.origin.x += frame2.size.width;
                _deleteButton.frame = frame2;
                [_confirmationView addSubview:_editButton];
                break;
            }
        }
        return;
    }
}

-(void)_editTap {
    UITableView *tv = (id)self.superview;
    while(tv && ![tv isKindOfClass:[UITableView class]]) {
        tv = (id)tv.superview;
    }
    id<UITableViewDelegate> delegate = tv.delegate;
    if([delegate respondsToSelector:@selector(tableView:editTappedForRowWithIndexPath:)]) {
        NSIndexPath *ip = [tv indexPathForCell:self];
        // define this in your own protocol
        [delegate tableView:tv editTappedForRowWithIndexPath:ip];
    }
}
@end

如果你能提供样例代码,我会非常高兴。谢谢。 - Guy Kahlon
完成。它可能有一两个错误,但你能理解大意。 - xtravar

1

有一个很棒的库叫做SwipeCellKit,它应该获得更多的认可。在我看来,它比MGSwipeTableCell更酷。后者无法完全复制邮件应用程序单元格的行为,而SwipeCellKit可以。看一下


我尝试了 SwipeCellKit 并感到印象深刻... 直到我遇到其中一个异常,因为在表格视图更新之前的行数与更新后的行数+/-行数变化不同。问题是,我从未更改过我的数据集。如果这不令人担忧,我不知道还有什么会让人担忧。所以我决定不使用它,而是使用新的 UITableViewDelegate 方法。如果您需要更多自定义,您可以始终覆盖 willBeginEditingRowAt: .... - horseshoe7
@horseshoe7 这很奇怪。我使用SwipeCellKit时从未遇到任何异常情况。毕竟,一个单元格与由数据源更改引起的这种异常有什么关系呢? - Andrey Chernukha

0

这里有一个简单的解决方案。它能够在UITableViewCell中显示和隐藏自定义UIView。 显示逻辑包含在从UITableViewCell扩展的类BaseTableViewCell中。

BaseTableViewCell.h

#import <UIKit/UIKit.h>

@interface BaseTableViewCell : UITableViewCell

@property(nonatomic,strong)UIView* customView;

-(void)showCustomView;

-(void)hideCustomView;

@end

BaseTableViewCell.M

#import "BaseTableViewCell.h"

@interface BaseTableViewCell()
{
    BOOL _isCustomViewVisible;
}

@end

@implementation BaseTableViewCell

- (void)awakeFromNib {
    // Initialization code
}

-(void)prepareForReuse
{
    self.customView = nil;
    _isCustomViewVisible = NO;
}

- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
    [super setSelected:selected animated:animated];

    // Configure the view for the selected state
}

-(void)showCustomView
{
    if(nil != self.customView)
    {
        if(!_isCustomViewVisible)
        {
            _isCustomViewVisible = YES;

            if(!self.customView.superview)
            {
                CGRect frame = self.customView.frame;
                frame.origin.x = self.contentView.frame.size.width;
                self.customView.frame = frame;
                [self.customView willMoveToSuperview:self.contentView];
                [self.contentView addSubview:self.customView];
                [self.customView didMoveToSuperview];
            }

            __weak BaseTableViewCell* blockSelf = self;
            [UIView animateWithDuration:.5 animations:^(){

                for(UIView* view in blockSelf.contentView.subviews)
                {
                    CGRect frame = view.frame;
                    frame.origin.x = frame.origin.x - blockSelf.customView.frame.size.width;
                    view.frame = frame;
                }
            }];
        }
    }
}

-(void)hideCustomView
{
    if(nil != self.customView)
    {
        if(_isCustomViewVisible)
        {
            __weak BaseTableViewCell* blockSelf = self;
            _isCustomViewVisible = NO;
            [UIView animateWithDuration:.5 animations:^(){
                for(UIView* view in blockSelf.contentView.subviews)
                {
                    CGRect frame = view.frame;
                    frame.origin.x = frame.origin.x + blockSelf.customView.frame.size.width;
                    view.frame = frame;
                }
            }];
        }
    }
}

@end

为了获得这个功能,只需从BaseTableViewCell扩展您的表视图单元格即可。
接下来,在实现UITableViewDelegate的UIViewController内部,创建两个手势识别器来处理左右滑动。
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    [self.tableView registerNib:[UINib nibWithNibName:CUSTOM_CELL_NIB_NAME bundle:nil] forCellReuseIdentifier:CUSTOM_CELL_ID];

    UISwipeGestureRecognizer* leftSwipeRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleLeftSwipe:)];
    leftSwipeRecognizer.direction = UISwipeGestureRecognizerDirectionLeft;
    [self.tableView addGestureRecognizer:leftSwipeRecognizer];

    UISwipeGestureRecognizer* rightSwipeRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleRightSwipe:)];
    rightSwipeRecognizer.direction = UISwipeGestureRecognizerDirectionRight;
    [self.tableView addGestureRecognizer:rightSwipeRecognizer];
}

然后添加两个滑动处理程序

- (void)handleLeftSwipe:(UISwipeGestureRecognizer*)recognizer
{
    CGPoint point = [recognizer locationInView:self.tableView];
    NSIndexPath* index = [self.tableView indexPathForRowAtPoint:point];

    UITableViewCell* cell = [self.tableView cellForRowAtIndexPath:index];

    if([cell respondsToSelector:@selector(showCustomView)])
    {
        [cell performSelector:@selector(showCustomView)];
    }
}

- (void)handleRightSwipe:(UISwipeGestureRecognizer*)recognizer
{
    CGPoint point = [recognizer locationInView:self.tableView];
    NSIndexPath* index = [self.tableView indexPathForRowAtPoint:point];

    UITableViewCell* cell = [self.tableView cellForRowAtIndexPath:index];

    if([cell respondsToSelector:@selector(hideCustomView)])
    {
        [cell performSelector:@selector(hideCustomView)];
    }
}

现在,在UITableViewDelegate的cellForRowAtIndexPath内部,您可以创建自定义UIView并将其附加到已排队的单元格。

-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    CustomCellTableViewCell* cell = (CustomCellTableViewCell*)[tableView dequeueReusableCellWithIdentifier:@"CustomCellTableViewCell" forIndexPath:indexPath];

    NSArray* nibViews = [[NSBundle mainBundle] loadNibNamed:@"CellCustomView"
                                                      owner:nil
                                                    options:nil];

    CellCustomView* customView = (CellCustomView*)[ nibViews objectAtIndex: 0];

    cell.customView = customView;

    return cell;
}

当然,这种自定义UIView的加载方式仅适用于此示例。您可以根据需要进行管理。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接