在 CollectionView 上滑动删除
Swipe to delete on CollectionView
我正在尝试复制 iOS 的滑动删除功能。我知道它可以在 tableview 上立即可用,但是我需要构建的 UI 受益于 Collection View。因此,我需要一个自定义实现,我将在其中使用向上滑动手势。幸运的是,这是我自己设法实现的,但是我很难弄清楚我需要如何设置滑动删除/点击删除/忽略功能。
UI 目前看起来是这样的:
所以我正在使用以下集合视图:
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
}
我的自定义视频单元包含一张图像,图像下方有删除按钮。因此,如果您向上滑动图像,则会弹出删除按钮。不确定这是否是正确的方法:
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")
}
}
我正在使用以下逻辑:
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.
}
}
但问题从这里开始。一旦我的单元格超出 collectionview 范围,我就无法再访问它。我仍然想进一步滑动它以将其删除。我只能通过在删除按钮上滑动来执行此操作,但我希望它上面的 Imageview 也可以滑动。或者,如果我点击 collectionview 外的图像,它应该滑回行中而不是删除它。
如果我增加 collectionview 边界,我可以防止这个问题,但我也可以滑动以移除单元格可见高度之外的区域。这是由 collectionview 内的 tapLocation 检测到 indexPath 引起的。我不想要的东西。我希望向上滑动仅适用于 collectionview 的单元格。
还有按钮和图片互相干扰,我分不清。它们都在同一个单元格中,所以我想知道我是否应该在单元格中放置删除按钮。或者我应该把它放在哪里?我还可以用它制作两个按钮并根据状态禁用用户交互,但不确定结果如何。
因此,如果您希望滑动手势识别器在其集合视图之外时继续记录移动,您需要将其附加到集合视图的父级,以便它绑定到用户所在的整个区域可以滑动
这确实意味着您将对集合视图之外的事物进行滑动,但您可以很容易地忽略那些使用任意数量的技术。
要注册删除按钮点击,您需要在按钮上调用 addTarget:action:forControlEvents:
我会保留您拥有的单元格,将图像和按钮放在一起。管理起来会容易很多,而且他们是一体的。
要管理上下移动图像,我会考虑使用转换或 NSLayoutConstraint。然后你只需要调整一个值,让它随着用户的滑动同步上下移动。不乱画框。
出于我自己的好奇心,我尝试复制您正在尝试做的事情,并让它以某种方式正常工作。它与你的不同之处在于我设置滑动手势的方式,因为我没有使用平移,但你说你已经有了那个部分,并且没有花太多时间在上面。 Pan 显然是使它具有交互性的更可靠的解决方案,但需要更长的时间来计算,但它的效果和处理方式与我的示例没有太大区别。
为了解决无法在单元格外滑动的问题,我决定检查该点是否在滑动的矩形中,它是非滑动矩形高度的两倍,如下所示:
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!])
}
我创建了一个带有评论的演示:https://github.com/imbue11235/swipeToDeleteCell
希望对你有所帮助!
如果你想让它更通用:
制作服装可滑动视图:
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
}
}
在 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")
}
}
样本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
}
}
我正在尝试复制 iOS 的滑动删除功能。我知道它可以在 tableview 上立即可用,但是我需要构建的 UI 受益于 Collection View。因此,我需要一个自定义实现,我将在其中使用向上滑动手势。幸运的是,这是我自己设法实现的,但是我很难弄清楚我需要如何设置滑动删除/点击删除/忽略功能。
UI 目前看起来是这样的:
所以我正在使用以下集合视图:
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
}
我的自定义视频单元包含一张图像,图像下方有删除按钮。因此,如果您向上滑动图像,则会弹出删除按钮。不确定这是否是正确的方法:
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")
}
}
我正在使用以下逻辑:
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.
}
}
但问题从这里开始。一旦我的单元格超出 collectionview 范围,我就无法再访问它。我仍然想进一步滑动它以将其删除。我只能通过在删除按钮上滑动来执行此操作,但我希望它上面的 Imageview 也可以滑动。或者,如果我点击 collectionview 外的图像,它应该滑回行中而不是删除它。
如果我增加 collectionview 边界,我可以防止这个问题,但我也可以滑动以移除单元格可见高度之外的区域。这是由 collectionview 内的 tapLocation 检测到 indexPath 引起的。我不想要的东西。我希望向上滑动仅适用于 collectionview 的单元格。
还有按钮和图片互相干扰,我分不清。它们都在同一个单元格中,所以我想知道我是否应该在单元格中放置删除按钮。或者我应该把它放在哪里?我还可以用它制作两个按钮并根据状态禁用用户交互,但不确定结果如何。
因此,如果您希望滑动手势识别器在其集合视图之外时继续记录移动,您需要将其附加到集合视图的父级,以便它绑定到用户所在的整个区域可以滑动
这确实意味着您将对集合视图之外的事物进行滑动,但您可以很容易地忽略那些使用任意数量的技术。
要注册删除按钮点击,您需要在按钮上调用 addTarget:action:forControlEvents:
我会保留您拥有的单元格,将图像和按钮放在一起。管理起来会容易很多,而且他们是一体的。
要管理上下移动图像,我会考虑使用转换或 NSLayoutConstraint。然后你只需要调整一个值,让它随着用户的滑动同步上下移动。不乱画框。
出于我自己的好奇心,我尝试复制您正在尝试做的事情,并让它以某种方式正常工作。它与你的不同之处在于我设置滑动手势的方式,因为我没有使用平移,但你说你已经有了那个部分,并且没有花太多时间在上面。 Pan 显然是使它具有交互性的更可靠的解决方案,但需要更长的时间来计算,但它的效果和处理方式与我的示例没有太大区别。
为了解决无法在单元格外滑动的问题,我决定检查该点是否在滑动的矩形中,它是非滑动矩形高度的两倍,如下所示:
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!])
}
我创建了一个带有评论的演示:https://github.com/imbue11235/swipeToDeleteCell
希望对你有所帮助!
如果你想让它更通用:
制作服装可滑动视图:
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
}
}
在 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")
}
}
样本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
}
}