Swipe to delete on CollectionView

2020-03-08 08:00发布

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.

3条回答
唯我独甜
2楼-- · 2020-03-08 08:09

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!

查看更多
混吃等死
3楼-- · 2020-03-08 08:16

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
}
}
查看更多
Luminary・发光体
4楼-- · 2020-03-08 08:22

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.

查看更多
登录 后发表回答