Swipe to delete on CollectionView
Asked Answered
A

4

24

I'm trying to replicate the swipe to delete functionality of iOS. I know it's instantly available on a tableview, but the UI that I need to build benefits from a Collection View. Therefor I need a custom implementation where I would be using a swipe up gesture. Luckily, that's something that I managed to implement myself, however I'm having a hard time figuring out how I need to setup the swipe to delete / tap to delete / ignore functionality.

The UI currently looks like this: Delete Image

So I'm using the following collectionview:

func buildCollectionView() {
    let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
    layout.scrollDirection = .horizontal
    layout.minimumInteritemSpacing = 0;
    layout.minimumLineSpacing = 4;

    collectionView = UICollectionView(frame: CGRect(x: 0, y: screenSize.midY - 120, width: screenSize.width, height: 180), collectionViewLayout: layout)

    collectionView.dataSource = self
    collectionView.delegate = self
    collectionView.register(VideoCell.self, forCellWithReuseIdentifier: "videoCell")
    collectionView.showsHorizontalScrollIndicator = false
    collectionView.showsVerticalScrollIndicator = false
    collectionView.contentInset = UIEdgeInsetsMake(0, 20, 0, 30)
    collectionView.backgroundColor = UIColor.white()
    collectionView.alpha = 0.0


    //can swipe cells outside collectionview region
    collectionView.layer.masksToBounds = false


    swipeUpRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.deleteCell))
    swipeUpRecognizer.delegate = self

    collectionView.addGestureRecognizer(swipeUpRecognizer)
    collectionView.isUserInteractionEnabled = true
}

My custom videocell contains one image and below that there is the delete button. So if you swipe the image up the delete button pops up. Not sure if this is the right way on how to do it:

class VideoCell : UICollectionViewCell {
var deleteView: UIButton!
var imageView: UIImageView!

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

    deleteView = UIButton(frame: CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height))
    deleteView.contentMode = UIViewContentMode.scaleAspectFit
    contentView.addSubview(deleteView)

    imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height))
    imageView.contentMode = UIViewContentMode.scaleAspectFit
    contentView.addSubview(imageView)


}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}
}

And I'm using the following logic:

func deleteCell(sender: UIPanGestureRecognizer) {
    let tapLocation = sender.location(in: self.collectionView)
    let indexPath = self.collectionView.indexPathForItem(at: tapLocation)

    if velocity.y < 0 { 
        //detect if there is a swipe up and detect it's distance. If the distance is far enough we snap the cells Imageview to the top otherwise we drop it back down. This works fine already.
    }
}

But the problem starts there. As soon as my cell is outside the collectionview bounds I can't access it anymore. I still want to swipe it further to remove it. I can only do this by swiping on the delete button, but I want the Imageview above it to be swipeable as well. Or if I tap the image outside the collectionview it should slide back into the line and not delete it.

If I increase the collectionview bounds I can prevent this problem but than I can also swipe to remove outside the cell's visible height. This is caused by the tapLocation that is inside the collectionview and detects an indexPath. Something that I don't want. I want the swipe up only to work on a collectionview's cell.

Also the button and the image interfere with each other because I cannot distinguish them. They are both in the same cell so that's why I'm wondering if I should have the delete button in the cell at all. Or where should I place it otherwise? I could also make two buttons out of it and disable user interaction depending on state, but not sure how that would end up.

Abbess answered 6/8, 2016 at 11:9 Comment(0)
C
3

For my own curiosity's sake I tried to make a replicate of what you're trying to do, and got it to work somehow good. It differs from yours in the way I setup the swipe gestures as I didn't use pan, but you said you already had that part, and haven't spend too much time on it. Pan is obviously the more solid solution to make it interactive, but takes a little longer to calculate, but the effect and handling of it, shouldn't differ much from my example.

To resolve the issue not being able to swipe outside the cell I decided to check if the point was in the swiped rect, which is twice the height of the non-swiped rect like this:

            let cellFrame = activeCell.frame
            let rect = CGRectMake(cellFrame.origin.x, cellFrame.origin.y - cellFrame.height, cellFrame.width, cellFrame.height*2)
            if CGRectContainsPoint(rect, point) {
                // If swipe point is in the cell delete it

                let indexPath = myView.indexPathForCell(activeCell)
                cats.removeAtIndex(indexPath!.row)
                myView.deleteItemsAtIndexPaths([indexPath!])

            }

I created a demonstration with comments: https://github.com/imbue11235/swipeToDeleteCell

I hope it helps you in anyway!

Congratulant answered 15/8, 2016 at 20:1 Comment(4)
I'm certainly interested in your solution. Could you push an updated version to github because I currently only get an empty project!Abbess
Oh yeah sure, that's a mistake, embarrassing. It's updated now!Congratulant
Works like a charm. Was exactly what I was looking for. Already rewrote it for my pangesture, but indeed I was looking on how to increase the cell area :)Abbess
Great to hear! Glad I could help :-)Congratulant
K
8

So, if you want the swipes gesture recogniser to continue recording movement when they are outside of their collection view, you need to attach it to the parent of the collection view, so it's bounded to the full area where the user can swipe.

That does mean that you will get swipes for things outside the collection view, but you can quite easily ignore those using any number of techniques.

To register delete button taps, you'll need to call addTarget:action:forControlEvents: on the button

I would keep the cell as you have it, with the image and the button together. It will be much easier to manage, and they belong together.

To manage moving the image up and down, I would look at using a transform, or an NSLayoutConstraint. Then you just have to adjust one value to make it move up and down in sync with the user swipes. No messing with frames.

Krusche answered 9/8, 2016 at 15:59 Comment(4)
Let me run down each alinea. If I attach it to the parent view of, like you said, I'll get swipes everywhere. Problem is once my cell get's outside my collectionview my indexpath in deletecell function will become nil and I can not drag the cell anymore. Increasing my collectionview size will make it possible but it will also enable users to drag cells around when they are not even touching the image itself. Delete action can be easily fixed. Only problem is, the imageview has a different action than the delete button. How to combine those two actions when they both visible?Abbess
I am not certain, I would imagine that disabling the collection view clips to bounds would allow you to transform the image view outside of it parents. myCollectionView.clipsToBounds = falseKrusche
Nope, that's not going to do the trick because a view will only forward "touch down" events to subviews (cells inside the collectionview) if they are within its bounds. So increasing the bounds will work but it's not what I want. Any other suggestions? I could override the pointInside method but that's something I don't want cause I think it won't return my indexpath.Abbess
Well, perhaps, as you said, have the UICollectionView be as large as the swipe-able area but use a custom UICollectionViewLayout to keep the cells in their original place, or contentInset from the UIScrollView parent class.Krusche
C
3

For my own curiosity's sake I tried to make a replicate of what you're trying to do, and got it to work somehow good. It differs from yours in the way I setup the swipe gestures as I didn't use pan, but you said you already had that part, and haven't spend too much time on it. Pan is obviously the more solid solution to make it interactive, but takes a little longer to calculate, but the effect and handling of it, shouldn't differ much from my example.

To resolve the issue not being able to swipe outside the cell I decided to check if the point was in the swiped rect, which is twice the height of the non-swiped rect like this:

            let cellFrame = activeCell.frame
            let rect = CGRectMake(cellFrame.origin.x, cellFrame.origin.y - cellFrame.height, cellFrame.width, cellFrame.height*2)
            if CGRectContainsPoint(rect, point) {
                // If swipe point is in the cell delete it

                let indexPath = myView.indexPathForCell(activeCell)
                cats.removeAtIndex(indexPath!.row)
                myView.deleteItemsAtIndexPaths([indexPath!])

            }

I created a demonstration with comments: https://github.com/imbue11235/swipeToDeleteCell

I hope it helps you in anyway!

Congratulant answered 15/8, 2016 at 20:1 Comment(4)
I'm certainly interested in your solution. Could you push an updated version to github because I currently only get an empty project!Abbess
Oh yeah sure, that's a mistake, embarrassing. It's updated now!Congratulant
Works like a charm. Was exactly what I was looking for. Already rewrote it for my pangesture, but indeed I was looking on how to increase the cell area :)Abbess
Great to hear! Glad I could help :-)Congratulant
H
0

If you want to make it mare generic:

Make a costume Swipeable View:

import UIKit

class SwipeView: UIView {
lazy var label: UILabel = {
    let label = UILabel()
    label.textColor = .black
    label.backgroundColor = .green
    return label
}()

let visableView = UIView()
var originalPoint: CGPoint!
var maxSwipe: CGFloat! = 50 {
    didSet(newValue) {
        maxSwipe = newValue
    }
}

@IBInspectable var swipeBufffer: CGFloat = 2.0
@IBInspectable var highVelocity: CGFloat = 300.0

private let originalXCenter: CGFloat = UIScreen.main.bounds.width / 2
private var panGesture: UIPanGestureRecognizer!

public var isPanGestureEnabled: Bool {
    get { return panGesture.isEnabled }
    set(newValue) {
        panGesture.isEnabled = newValue
    }
}

override init(frame: CGRect) {
    super.init(frame: frame)
    setupViews()
    setupGesture()
}

private func setupViews() {
    addSubview(visableView)
    visableView.addSubview(label)

    visableView.edgesToSuperview()
    label.edgesToSuperview()

}

private func setupGesture() {
    panGesture = UIPanGestureRecognizer(target: self, action: #selector(swipe(_:)))
    panGesture.delegate = self
    addGestureRecognizer(panGesture)
}

@objc func swipe(_ sender:UIPanGestureRecognizer) {
    let translation = sender.translation(in: self)
    let newXPosition = center.x + translation.x
    let velocity = sender.velocity(in: self)

    switch(sender.state) {

    case .changed:
        let shouldSwipeRight = translation.x > 0 && newXPosition < originalXCenter
        let shouldSwipeLeft = translation.x < 0 && newXPosition > originalXCenter - maxSwipe
        guard shouldSwipeRight || shouldSwipeLeft else { break }
            center.x = newXPosition
    case .ended:
        if -velocity.x > highVelocity {
            center.x = originalXCenter - maxSwipe
            break
        }
        guard center.x > originalXCenter - maxSwipe - swipeBufffer, center.x < originalXCenter - maxSwipe + swipeBufffer, velocity.x < highVelocity  else {
            center.x = originalXCenter
            break
        }
    default:
        break
    }
    panGesture.setTranslation(.zero, in: self)
}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

}

extension SwipeView: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    return true
}
}

The embed swappable view in UICollectionViewCell:

import UIKit
import TinyConstraints

protocol DeleteCellDelegate {
func deleteCell(_ sender : UIButton)
}

class SwipeableCell: UICollectionViewCell {

lazy var deleteButton: UIButton = {
    let button = UIButton()
    button.backgroundColor = .red
    button.addTarget(self, action: #selector(didPressedButton(_:)), for: .touchUpInside)
    button.titleLabel?.text = "Delete"
    return button
}()

var deleteCellDelegate: DeleteCellDelegate?

@objc private func didPressedButton(_ sender: UIButton) {
    deleteCellDelegate?.deleteCell(sender)
    print("delete")
}
let swipeableview: SwipeView = {
    return SwipeView()
}()

override init(frame: CGRect) {
    super.init(frame: frame)
    addSubview(deleteButton)
    addSubview(swipeableview)
    swipeableview.edgesToSuperview()

    deleteButton.edgesToSuperview(excluding: .left, usingSafeArea: true)
    deleteButton.width(bounds.width * 0.3)
    swipeableview.maxSwipe = deleteButton.bounds.width

}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}
}

A sample ViewController:

import UIKit
import TinyConstraints

class ViewController: UIViewController, DeleteCellDelegate {

func deleteCell(_ sender: UIButton) {
    let indexPath = IndexPath(item: sender.tag, section: 0)
    items.remove(at: sender.tag)
    collectionView.deleteItems(at: [indexPath])
}

lazy var collectionView: UICollectionView = {
    let layout = UICollectionViewFlowLayout()
    layout.itemSize = CGSize(width: view.bounds.width, height: 40)
    layout.sectionInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
    let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
    cv.backgroundColor = .yellow
    cv.isPagingEnabled = true
    cv.isUserInteractionEnabled = true
    return cv
}()

var items  = ["1", "2", "3"]

override func viewDidLoad() {
    super.viewDidLoad()
    view.addSubview(collectionView)
    collectionView.delegate = self
    collectionView.dataSource = self
    collectionView.edgesToSuperview(usingSafeArea: true)
    collectionView.register(SwipeableCell.self, forCellWithReuseIdentifier: "cell")
    let panGesture = UIPanGestureRecognizer()
    view.addGestureRecognizer(panGesture)
    panGesture.delegate = self
}

}

extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return items.count
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! SwipeableCell
    cell.backgroundColor = .blue
    cell.swipeableview.label.text = items[indexPath.item]
    cell.deleteButton.tag = indexPath.item
    cell.deleteCellDelegate = self
    return cell
}

func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) {
}

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
}

}

extension ViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    return false
}
}
Halfway answered 28/4, 2019 at 10:33 Comment(0)
C
0

// Enable swipe-to-delete functionality

func collectionView(_ collectionView: UICollectionView, canEditItemAt indexPath: IndexPath) -> Bool {
    return true
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
    return 0
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
    return 0
}

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    // Handle item selection if needed
}

func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    // Configure swipe-to-delete gesture recognizer for the cell
    let swipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipeGesture(_:)))
    swipeGesture.direction = .right
    cell.addGestureRecognizer(swipeGesture)
}

@objc func handleSwipeGesture(_ gestureRecognizer: UISwipeGestureRecognizer) {
    guard let indexPath = collectionView.indexPathForItem(at: gestureRecognizer.location(in: collectionView)) else {
        return
    }
    self.deleteItemAt(indexPath: indexPath)
}

private func deleteItemAt(indexPath: IndexPath) {
    selectedCityNames.remove(at: indexPath.item)
    collectionView.deleteItems(at: [indexPath])
}  
Conrado answered 18/5, 2023 at 12:23 Comment(1)
Keep in mind that willDisplay can be called many times on a cell. Your code will add another gesture each time. That's a bad place to add the gesture.Anodic

© 2022 - 2024 — McMap. All rights reserved.