【问题标题】:How to implement UITableView`s swipe to delete for UICollectionView如何为 UICollectionView 实现 UITableView 的滑动删除
【发布时间】:2012-12-25 13:18:45
【问题描述】:

我只是想问如何在 UICollectionView 中实现与 UITableView 的滑动删除相同的行为。我正在寻找教程,但找不到任何教程。

另外,我正在使用 PSTCollectionView 包装器来支持 iOS 5。

谢谢!

编辑: 滑动识别器已经很好了。 我现在需要的是取消删除模式时与 UITableView 相同的功能,例如当用户点击表格视图中的单元格或空白区域时(即,当用户在“删除”按钮之外点击时)。 UITapGestureRecognizer 不起作用,因为它只检测释放触摸时的点击。 UITableView 在手势开始时检测到触摸(而不是在释放时),并立即取消删除模式。

【问题讨论】:

    标签: ios uitableview uicollectionview


    【解决方案1】:

    对于您的问题,有一个更简单的解决方案,可以避免使用手势识别器。该解决方案基于UIScrollView 结合UIStackView

    1. 首先,您需要创建 2 个容器视图 - 一个用于单元格的可见部分,一个用于隐藏部分。您会将这些视图添加到 UIStackViewstackView 将充当内容视图。确保视图与stackView.distribution = .fillEqually 具有相同的宽度。

    2. 您将在启用分页的UIScrollView 中嵌入stackViewscrollView 应限制在单元格的边缘。然后将stackView 的宽度设置为scrollView 宽度的2 倍,这样每个容器视图都将具有单元格的宽度。

    通过这个简单的实现,您已经创建了具有可见和隐藏视图的基本单元。使用可见视图向单元格添加内容,在隐藏视图中您可以添加删除按钮。这样你就可以实现:

    我已经设置了example project on GitHub。你也可以read more about this solution here.
    此解决方案的最大优点是简单,您不必处理约束和手势识别器。

    【讨论】:

    • 很好的解决方案。这种方法使得可以滑动多个单元格同时显示 hiddenView。您将如何启用单个单元格选择?将 scrollView.setContentOffset 设置为零似乎没有帮助。
    • 另外,当在网格上实现时,删除一个单元格会意外地打开一些重新洗牌的单元格(即显示删除按钮)。有什么见解吗?谢谢!
    • @Plutovman 删除单元格时,collectionview 会重新加载单元格,并且打开删除按钮的单元格将被重用。您需要记住当前打开的单元格的 indexPath,当单元格出列时,您需要检查 indexPath 是否与打开的单元格的 indexPath 匹配,并通过设置滚动视图的内容偏移量来打开它。当它被删除时,将 indexPath 设置为 nil。
    • 你是绝对正确的。再次,在优雅的解决方案上做得很好。感觉 UICollectionView 应该内置了这个功能。
    • @AmerHukic 嘿。很好的解决方案。但是,我不确定您将 indexPath 设置为 nil 是什么意思,介意详细说明一下吗?因为每次删除和重用单元格时,视图都停留在隐藏视图中。我试图弄清楚如何解决它。谢谢。
    【解决方案2】:

    iOS 的集合视图编程指南中,Incorporating Gesture Support 部分的文档如下:

    您应该始终将手势识别器附加到集合视图本身,而不是特定的单元格或视图。

    所以,我认为将识别器添加到UICollectionViewCell 不是一个好习惯。

    【讨论】:

      【解决方案3】:

      很简单。你需要在customContentView后面加上customContentViewcustomBackgroundView

      之后,当用户从右向左滑动时,您需要将customContentView 向左移动。移动视图使customBackgroundView 可见。

      让我们编写代码:

      首先你需要将 panGesture 添加到你的UICollectionView

         override func viewDidLoad() {
              super.viewDidLoad()
              self.panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.panThisCell))
              panGesture.delegate = self
              self.collectionView.addGestureRecognizer(panGesture)
      
          }
      

      现在将选择器实现为

        func panThisCell(_ recognizer:UIPanGestureRecognizer){
      
              if recognizer != panGesture{  return }
      
              let point = recognizer.location(in: self.collectionView)
              let indexpath = self.collectionView.indexPathForItem(at: point)
              if indexpath == nil{  return }
              guard let cell = self.collectionView.cellForItem(at: indexpath!) as? CustomCollectionViewCell else{
      
                  return
      
              }
              switch recognizer.state {
              case .began:
      
                  cell.startPoint =  self.collectionView.convert(point, to: cell)
                  cell.startingRightLayoutConstraintConstant  = cell.contentViewRightConstraint.constant
                  if swipeActiveCell != cell && swipeActiveCell != nil{
      
                      self.resetConstraintToZero(swipeActiveCell!,animate: true, notifyDelegateDidClose: false)
                  }
                  swipeActiveCell = cell
      
              case .changed:
      
                  let currentPoint =  self.collectionView.convert(point, to: cell)
                  let deltaX = currentPoint.x - cell.startPoint.x
                  var panningleft = false
      
                  if currentPoint.x < cell.startPoint.x{
      
                      panningleft = true
      
                  }
                  if cell.startingRightLayoutConstraintConstant == 0{
      
                      if !panningleft{
      
                          let constant = max(-deltaX,0)
                          if constant == 0{
      
                              self.resetConstraintToZero(cell,animate: true, notifyDelegateDidClose: false)
      
                          }else{
      
                              cell.contentViewRightConstraint.constant = constant
      
                          }
                      }else{
      
                          let constant = min(-deltaX,self.getButtonTotalWidth(cell))
                          if constant == self.getButtonTotalWidth(cell){
      
                              self.setConstraintsToShowAllButtons(cell,animate: true, notifyDelegateDidOpen: false)
      
                          }else{
      
                              cell.contentViewRightConstraint.constant = constant
                              cell.contentViewLeftConstraint.constant = -constant
                          }
                      }
                  }else{
      
                      let adjustment = cell.startingRightLayoutConstraintConstant - deltaX;
                      if (!panningleft) {
      
                          let constant = max(adjustment, 0);
                          if (constant == 0) {
      
                              self.resetConstraintToZero(cell,animate: true, notifyDelegateDidClose: false)
      
                          } else {
      
                              cell.contentViewRightConstraint.constant = constant;
                          }
                      } else {
                          let constant = min(adjustment, self.getButtonTotalWidth(cell));
                          if (constant == self.getButtonTotalWidth(cell)) {
      
                              self.setConstraintsToShowAllButtons(cell,animate: true, notifyDelegateDidOpen: false)
                          } else {
      
                              cell.contentViewRightConstraint.constant = constant;
                          }
                      }
                      cell.contentViewLeftConstraint.constant = -cell.contentViewRightConstraint.constant;
      
                  }
                  cell.layoutIfNeeded()
              case .cancelled:
      
                  if (cell.startingRightLayoutConstraintConstant == 0) {
      
                      self.resetConstraintToZero(cell,animate: true, notifyDelegateDidClose: true)
      
                  } else {
      
                      self.setConstraintsToShowAllButtons(cell,animate: true, notifyDelegateDidOpen: true)
                  }
      
              case .ended:
      
                  if (cell.startingRightLayoutConstraintConstant == 0) {
                      //Cell was opening
                      let halfOfButtonOne = (cell.swipeView.frame).width / 2;
                      if (cell.contentViewRightConstraint.constant >= halfOfButtonOne) {
                          //Open all the way
                          self.setConstraintsToShowAllButtons(cell,animate: true, notifyDelegateDidOpen: true)
                      } else {
                          //Re-close
                          self.resetConstraintToZero(cell,animate: true, notifyDelegateDidClose: true)
                      }
                  } else {
                      //Cell was closing
                      let buttonOnePlusHalfOfButton2 = (cell.swipeView.frame).width
                      if (cell.contentViewRightConstraint.constant >= buttonOnePlusHalfOfButton2) {
                          //Re-open all the way
                          self.setConstraintsToShowAllButtons(cell,animate: true, notifyDelegateDidOpen: true)
                      } else {
                          //Close
                          self.resetConstraintToZero(cell,animate: true, notifyDelegateDidClose: true)
                      }
                  }
      
              default:
                  print("default")
              }
          }
      

      更新约束的辅助方法

       func getButtonTotalWidth(_ cell:CustomCollectionViewCell)->CGFloat{
      
              let width = cell.frame.width - cell.swipeView.frame.minX
              return width
      
          }
      
          func resetConstraintToZero(_ cell:CustomCollectionViewCell, animate:Bool,notifyDelegateDidClose:Bool){
      
              if (cell.startingRightLayoutConstraintConstant == 0 &&
                  cell.contentViewRightConstraint.constant == 0) {
                  //Already all the way closed, no bounce necessary
                  return;
              }
              cell.contentViewRightConstraint.constant = -kBounceValue;
              cell.contentViewLeftConstraint.constant = kBounceValue;
              self.updateConstraintsIfNeeded(cell,animated: animate) {
                  cell.contentViewRightConstraint.constant = 0;
                  cell.contentViewLeftConstraint.constant = 0;
      
                  self.updateConstraintsIfNeeded(cell,animated: animate, completionHandler: {
      
                      cell.startingRightLayoutConstraintConstant = cell.contentViewRightConstraint.constant;
                  })
              }
              cell.startPoint = CGPoint()
              swipeActiveCell = nil
          }
      
          func setConstraintsToShowAllButtons(_ cell:CustomCollectionViewCell, animate:Bool,notifyDelegateDidOpen:Bool){
      
              if (cell.startingRightLayoutConstraintConstant == self.getButtonTotalWidth(cell) &&
                  cell.contentViewRightConstraint.constant == self.getButtonTotalWidth(cell)) {
                  return;
              }
              cell.contentViewLeftConstraint.constant = -self.getButtonTotalWidth(cell) - kBounceValue;
              cell.contentViewRightConstraint.constant = self.getButtonTotalWidth(cell) + kBounceValue;
      
              self.updateConstraintsIfNeeded(cell,animated: animate) {
                  cell.contentViewLeftConstraint.constant =  -(self.getButtonTotalWidth(cell))
                  cell.contentViewRightConstraint.constant = self.getButtonTotalWidth(cell)
      
                  self.updateConstraintsIfNeeded(cell,animated: animate, completionHandler: {(check) in
      
                      cell.startingRightLayoutConstraintConstant = cell.contentViewRightConstraint.constant;
                  })
              }
          }
      
          func setConstraintsAsSwipe(_ cell:CustomCollectionViewCell, animate:Bool,notifyDelegateDidOpen:Bool){
      
              if (cell.startingRightLayoutConstraintConstant == self.getButtonTotalWidth(cell) &&
                  cell.contentViewRightConstraint.constant == self.getButtonTotalWidth(cell)) {
                  return;
              }
              cell.contentViewLeftConstraint.constant = -self.getButtonTotalWidth(cell) - kBounceValue;
              cell.contentViewRightConstraint.constant = self.getButtonTotalWidth(cell) + kBounceValue;
      
              self.updateConstraintsIfNeeded(cell,animated: animate) {
                  cell.contentViewLeftConstraint.constant =  -(self.getButtonTotalWidth(cell))
                  cell.contentViewRightConstraint.constant = self.getButtonTotalWidth(cell)
      
                  self.updateConstraintsIfNeeded(cell,animated: animate, completionHandler: {(check) in
      
                      cell.startingRightLayoutConstraintConstant = cell.contentViewRightConstraint.constant;
                  })
              }
          }
      
      
          func updateConstraintsIfNeeded(_ cell:CustomCollectionViewCell, animated:Bool,completionHandler:@escaping ()->()) {
              var duration:Double = 0
              if animated{
      
                  duration = 0.1
      
              }
              UIView.animate(withDuration: duration, delay: 0, options: [.curveEaseOut], animations: {
      
                  cell.layoutIfNeeded()
      
                  }, completion:{ value in
      
                      if value{ completionHandler() }
              })
          }
      

      我在 Swift 3 中创建了一个示例项目 here

      这是tutorial的修改版。

      【讨论】:

      • 优秀的例子,这是你应该在 Swift 3 和 2017+ 中使用的例子
      • 感觉和 UITableView 很不一样。还存在多个单元格可能处于滑动状态的故障。然后,当点击第一次刷卡的单元格时,它会崩溃。我不建议在未正确修复的情况下采用和使用此解决方案。 iOS 14 增加了原生 API,见 leadingSwipeActionsConfigurationProvidertrailingSwipeActionsConfigurationProvider
      【解决方案4】:

      我采用了与@JacekLampart 类似的方法,但决定在 UICollectionViewCell 的 awakeFromNib 函数中添加 UISwipeGestureRecognizer,因此它只添加一次。

      UICollectionViewCell.m

      - (void)awakeFromNib {
          UISwipeGestureRecognizer* swipeGestureRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeToDeleteGesture:)];
          swipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionLeft;
          [self addGestureRecognizer:swipeGestureRecognizer];
      }
      
      - (void)swipeToDeleteGesture:(UISwipeGestureRecognizer *)swipeGestureRecognizer {
          if (swipeGestureRecognizer.state == UIGestureRecognizerStateEnded) {
              // update cell to display delete functionality
          }
      }
      

      至于退出删除模式,我创建了一个自定义 UIGestureRecognizer 和一个 NSArray 的 UIViews。我从这个问题中借用了@iMS 的想法:UITapGestureRecognizer - make it work on touch down, not touch up?

      在 touchesBegan 时,如果触摸点不在任何 UIView 中,则手势成功并退出删除模式。

      通过这种方式,我可以将单元格(和任何其他视图)中的删除按钮传递给 UIGestureRecognizer,如果触摸点在按钮的框架内,删除模式将不会退出。

      TouchDownExcludingViewsGestureRecognizer.h

      #import <UIKit/UIKit.h>
      
      @interface TouchDownExcludingViewsGestureRecognizer : UIGestureRecognizer
      
      @property (nonatomic) NSArray *excludeViews;
      
      @end
      

      TouchDownExcludingViewsGestureRecognizer.m

      #import "TouchDownExcludingViewsGestureRecognizer.h"
      #import <UIKit/UIGestureRecognizerSubclass.h>
      
      @implementation TouchDownExcludingViewsGestureRecognizer
      
      - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
          if (self.state == UIGestureRecognizerStatePossible) {
              BOOL touchHandled = NO;
              for (UIView *view in self.excludeViews) {
                  CGPoint touchLocation = [[touches anyObject] locationInView:view];
                  if (CGRectContainsPoint(view.bounds, touchLocation)) {
                      touchHandled = YES;
                      break;
                  }
              }
      
              self.state = (touchHandled ? UIGestureRecognizerStateFailed : UIGestureRecognizerStateRecognized);
          }
      }
      
      - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
          self.state = UIGestureRecognizerStateFailed;
      }
      
      -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
          self.state = UIGestureRecognizerStateFailed;
      }
      
      
      @end
      

      实现(在包含 UICollectionView 的 UIViewController 中):

      #import "TouchDownExcludingViewsGestureRecognizer.h"
      
      TouchDownExcludingViewsGestureRecognizer *touchDownGestureRecognizer = [[TouchDownExcludingViewsGestureRecognizer alloc] initWithTarget:self action:@selector(exitDeleteMode:)];
      touchDownGestureRecognizer.excludeViews = @[self.cellInDeleteMode.deleteButton];
      [self.view addGestureRecognizer:touchDownGestureRecognizer];
      
      - (void)exitDeleteMode:(TouchDownExcludingViewsGestureRecognizer *)touchDownGestureRecognizer {
          // exit delete mode and disable or remove TouchDownExcludingViewsGestureRecognizer
      }
      

      【讨论】:

        【解决方案5】:

        您可以尝试将 UISwipeGestureRecognizer 添加到每个集合单元格,如下所示:

        -(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
                     cellForItemAtIndexPath:(NSIndexPath *)indexPath
        {
            CollectionViewCell *cell = ...
        
            UISwipeGestureRecognizer* gestureRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(userDidSwipe:)];
            [gestureRecognizer setDirection:UISwipeGestureRecognizerDirectionRight];
            [cell addGestureRecognizer:gestureRecognizer];
        }
        

        接着是:

        - (void)userDidSwipe:(UIGestureRecognizer *)gestureRecognizer {
            if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
                //handle the gesture appropriately
            }
        }
        

        【讨论】:

        • 感谢您的回复!然而,UITableView 的滑动删除允许用户在触摸屏幕时取消删除。我似乎无法正确实施。有什么建议吗?
        • 同理:在“适当处理手势”部分创建一个UITapGestureRecognizer。
        【解决方案6】:

        在 iOS 14 中,您可以将 UICollectionViewLayoutListConfigurationUICollectionViewCompositionalLayout 结合使用以免费获得本机功能,无需自定义单元格或手势识别。

        如果您的最低部署目标是 >= iOS 14.x,这可能是从现在开始的首选方法,它还可以让您采用带有UIContentViewUIContentConfiguration 的现代单元配置来启动。

        【讨论】:

          【解决方案7】:

          有一个更标准的解决方案来实现此功能,其行为与UITableView 提供的行为非常相似。

          为此,您将使用UIScrollView 作为单元格的根视图,然后将单元格内容和删除按钮放置在滚动视图内。单元类中的代码应该是这样的:

          override init(frame: CGRect) {
              super.init(frame: frame)
          
              addSubview(scrollView)
              scrollView.addSubview(viewWithCellContent)
              scrollView.addSubview(deleteButton)
              scrollView.isPagingEnabled = true
              scrollView.showsHorizontalScrollIndicator = false
          }
          

          在这段代码中,我们将属性isPagingEnabled 设置为true 以使滚动视图仅在其内容的边界处停止滚动。这个单元格的布局子视图应该是这样的:

          override func layoutSubviews() {
              super.layoutSubviews()
          
              scrollView.frame = bounds
              // make the view with the content to fill the scroll view
              viewWithCellContent.frame = scrollView.bounds
              // position the delete button just at the right of the view with the content.
              deleteButton.frame = CGRect(
                  x: label.frame.maxX, 
                  y: 0, 
                  width: 100, 
                  height: scrollView.bounds.height
              )
          
              // update the size of the scrolleable content of the scroll view
              scrollView.contentSize = CGSize(width: button.frame.maxX, height: scrollView.bounds.height)
          }
          

          使用此代码后,如果您运行应用程序,您会看到滑动删除操作按预期工作,但是,我们失去了选择单元格的能力。问题是,由于滚动视图填充了整个单元格,所有的触摸事件都由它处理,所以集合视图永远没有机会选择单元格(这类似于我们在单元格内有一个按钮时,因为触摸该按钮不会触发选择过程,而是由按钮直接处理。)

          要解决这个问题,我们只需要指示滚动视图忽略由它而不是它的子视图之一处理的触摸事件。要实现这一点,只需创建UIScrollView 的子类并覆盖以下函数:

          override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
              let result = super.hitTest(point, with: event)
              return result != self ? result : nil
          }
          

          现在在您的单元格中,您应该使用这个新子类的实例,而不是标准的 UIScrollView

          如果您现在运行该应用程序,您会看到我们恢复了单元格选择,但这次滑动不起作用?。由于我们忽略了由滚动视图直接处理的触摸,因此它的平移手势识别器将无法开始识别触摸事件。但是,可以通过向滚动视图指示其平移手势识别器将由单元格而不是滚动处理来轻松解决此问题。为此,您在单元格的 init(frame: CGRect) 底部添加以下行:

          addGestureRecognizer(scrollView.panGestureRecognizer)
          

          这可能看起来有点老套,但事实并非如此。根据设计,包含手势识别器的视图和该识别器的目标不必是同一个对象。

          进行此更改后,一切都应按预期工作。你可以看到这个想法的完整实现in this repo

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2015-03-09
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-07-07
            • 1970-01-01
            相关资源
            最近更新 更多