24

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

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

谢谢!

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

4

7 回答 7

18

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

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

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

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

滑动删除

在 GitHub 上建立了一个示例项目。您还可以在此处阅读有关此解决方案的更多信息
此解决方案的最大优点是简单,您不必处理约束和手势识别器。

于 2018-09-16T09:17:30.117 回答
14

customContentView它非常简单..您需要customBackgroundViewcustomContentView.

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

让代码:

首先,您需要将 panGesture 添加到您的UICollectionViewas

   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 中创建了一个示例项目。

它是本教程的修改版本。

于 2016-11-07T12:49:15.187 回答
13

Collection View Programming Guide for iOSIncorporating Gesture Support部分中,文档如下:

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

所以,我认为将识别器添加到UICollectionViewCell.

于 2014-08-09T13:16:37.990 回答
3

我采用了与@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 - 让它在触地时工作,而不是触地?

在 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
}
于 2014-02-14T00:58:00.163 回答
2

您可以尝试将 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
    }
}
于 2013-01-11T01:18:43.707 回答
2

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

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

于 2020-12-31T23:22:54.647 回答
1

有一个更标准的解决方案来实现此功能,其行为与UITableView.

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

override init(frame: CGRect) {
    super.init(frame: frame)

    addSubview(scrollView)
    scrollView.addSubview(viewWithCellContent)
    scrollView.addSubview(deleteButton)
    scrollView.isPagingEnabled = true
    scrollView.showsHorizontalScrollIndicator = false
}

在这段代码中,我们将属性设置isPagingEnabledtrue使滚动视图仅在其内容的边界处停止滚动。此单元格的布局子视图应类似于:

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)

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

在此更改之后,一切都应该按预期工作。你可以在这个 repo中看到这个想法的完整实现

于 2018-08-16T16:29:57.777 回答