How to implement UITableView`s swipe to delete for UICollectionView

前端 未结 6 502
你的背包
你的背包 2020-12-08 10:34

I just like to ask how can I implement the same behavior of UITableView`s swipe to delete in UICollectionView. I am trying to find a tutorial but I cannot find any.

相关标签:
6条回答
  • 2020-12-08 11:11

    There is a simpler solution to your problem that avoids using gesture recognizers. The solution is based on UIScrollView in combination with UIStackView.

    1. First, you need to create 2 container views - one for the visible part of the cell and one for the hidden part. You’ll add these views to a UIStackView. The stackView will act as a content view. Make sure that the views have equal widths with stackView.distribution = .fillEqually.

    2. You’ll embed the stackView inside a UIScrollView that has paging enabled. The scrollView should be constrained to the edges of the cell. Then you’ll set the stackView’s width to be 2 times the scrollView’s width so each of the container views will have the width of the cell.

    With this simple implementation, you have created the base cell with a visible and hidden view. Use the visible view to add content to the cell and in the hidden view you can add a delete button. This way you can achieve this:

    I've set up an example project on GitHub. You can also read more about this solution here.
    The biggest advantage of this solution is the simplicity and that you don't have to deal with constraints and gesture recognizers.

    0 讨论(0)
  • 2020-12-08 11:20

    In the Collection View Programming Guide for iOS, in the section Incorporating Gesture Support, the docs read:

    You should always attach your gesture recognizers to the collection view itself and not to a specific cell or view.

    So, I think it's not a good practice to add recognizers to UICollectionViewCell.

    0 讨论(0)
  • 2020-12-08 11:22

    You can try adding a UISwipeGestureRecognizer to each collection cell, like this:

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

    followed by:

    - (void)userDidSwipe:(UIGestureRecognizer *)gestureRecognizer {
        if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
            //handle the gesture appropriately
        }
    }
    
    0 讨论(0)
  • 2020-12-08 11:22

    There is a more standard solution to implement this feature, having a behavior very similar to the one provided by UITableView.

    For this, you will use a UIScrollView as the root view of the cell, and then position the cell content and the delete button inside the scroll view. The code in your cell class should be something like this:

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

    In this code we set the property isPagingEnabled to true to make the scroll view to stop scrolling only at the boundaries of its content. The layout subviews for this cell should be something like:

    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)
    }
    

    With this code in place, if you run the app you will see that the swipe to delete is working as expected, however, we lost the ability to select the cell. The problem is that since the scroll view is filling the whole cell, all the touch events are processed by it, so the collection view will never have the opportunity to select the cell (this is similar to when we have a button inside a cell, since touches on that button don't trigger the selection process but are handled directly by the button.)

    To fix this problem we just have to indicate the scroll view to ignore the touch events that are processed by it and not by one of its subviews. To achieve this just create a subclass of UIScrollView and override the following function:

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

    Now in your cell you should use an instance of this new subclass instead of the standard UIScrollView.

    If you run the app now you will see that we have the cell selection back, but this time the swipe isn't working

    0 讨论(0)
  • 2020-12-08 11:33

    I followed a similar approach to @JacekLampart, but decided to add the UISwipeGestureRecognizer in the UICollectionViewCell's awakeFromNib function so it is only added once.

    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
        }
    }
    

    As for exiting delete mode, I created a custom UIGestureRecognizer with an NSArray of UIViews. I borrowed the idea from @iMS from this question: UITapGestureRecognizer - make it work on touch down, not touch up?

    On touchesBegan, if the touch point isn't within any of the UIViews, the gesture succeeds and delete mode is exited.

    In this way, I am able to pass the delete button within the cell (and any other views) to the UIGestureRecognizer and, if the touch point is within the button's frame, delete mode will not exit.

    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
    

    Implementation (in the UIViewController containing UICollectionView):

    #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
    }
    
    0 讨论(0)
  • 2020-12-08 11:35

    Its very simple..You need to add a customContentView and customBackgroundView behind the customContentView.

    After that and you need to shift the customContentViewto the left as user swipes from right to left. Shifting the view makes visible to the customBackgroundView.

    Lets Code:

    First of all you need to add panGesture to your UICollectionView as

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

    Now implement the selector as

      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")
            }
        }
    

    Helper methods to update constraints

     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() }
            })
        }
    

    I have created a sample project here in Swift 3.

    It is a modified version of this tutorial.

    0 讨论(0)
提交回复
热议问题