ios 如何为 UICollectionView 实现 UITableView 的滑动删除
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/14270023/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to implement UITableView`s swipe to delete for UICollectionView
提问by MiuMiu
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.
我只是想问我如何在 UICollectionView 中实现 UITableView 的滑动删除相同的行为。我正在寻找教程,但找不到任何教程。
Also, I am using PSTCollectionView wrapper to support iOS 5.
另外,我使用 PSTCollectionView 包装器来支持 iOS 5。
Thank you!
谢谢!
Edit: The swipe recognizer is already good. What I need now is the same functionality as UITableView's when cancelling the Delete mode, e.g. when user taps on a cell or on a blank space in the table view (that is, when user taps outside of the Delete button). UITapGestureRecognizer won't work, since it only detects taps on release of a touch. UITableView detects a touch on begin of the gesture (and not on release), and immediately cancels the Delete mode.
编辑:滑动识别器已经很好了。我现在需要的是取消删除模式时与 UITableView 相同的功能,例如,当用户点击表格视图中的单元格或空白区域时(即,当用户点击删除按钮外部时)。UITapGestureRecognizer 不起作用,因为它只检测释放触摸时的点击。UITableView 检测到手势开始时的触摸(而不是释放时),并立即取消删除模式。
回答by Phat Le
In the Collection View Programming Guide for iOS, in the section Incorporating Gesture Support, the docs read:
在iOS的Collection View Programming Guide 中,在Incorporating Gesture Support部分,文档内容如下:
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
.
因此,我认为将识别器添加到UICollectionViewCell
.
回答by Anish Parajuli ?
Its very simple..You need to add a customContentView
and customBackgroundView
behind the customContentView
.
它非常简单..您需要在.后面添加一个customContentView
和。customBackgroundView
customContentView
After that and you need to shift the customContentView
to the left as user swipes from right to left. Shifting the view makes visible to the customBackgroundView
.
之后,您需要在customContentView
用户从右向左滑动时向左移动。移动视图使customBackgroundView
.
Lets Code:
让我们代码:
First of all you need to add panGesture to your UICollectionView
as
首先,您需要将 panGesture 添加到您的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 herein Swift 3.
我创建了一个示例项目在这里斯威夫特3。
It is a modified version of this tutorial.
它是本教程的修改版本。
回答by Amer Hukic
There is a simpler solution to your problem that avoids using gesture recognizers. The solution is based on UIScrollView
in combination with UIStackView
.
有一个更简单的解决方案可以避免使用手势识别器。该解决方案基于UIScrollView
与UIStackView
.
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
. ThestackView
will act as a content view. Make sure that the views have equal widths withstackView.distribution = .fillEqually
.You'll embed the
stackView
inside aUIScrollView
that has paging enabled. ThescrollView
should be constrained to the edges of the cell. Then you'll set thestackView
's width to be 2 times thescrollView
's width so each of the container views will have the width of the cell.
首先,您需要创建 2 个容器视图 - 一个用于单元格的可见部分,一个用于隐藏部分。您将这些视图添加到
UIStackView
. 该stackView
会作为一个内容视图。确保视图的宽度与stackView.distribution = .fillEqually
.您将嵌入已启用分页的
stackView
内部UIScrollView
。的scrollView
应限制在小区的边缘。然后,您将stackView
的宽度设置为的宽度的 2 倍,scrollView
以便每个容器视图都具有单元格的宽度。
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.
我已经在 GitHub 上建立了一个示例项目。您还可以在此处阅读有关此解决方案的更多信息。
此解决方案的最大优点是简单,您不必处理约束和手势识别器。
回答by Kristian Bauer
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.
我对@JacekLampart 采取了类似的方法,但决定在 UICollectionViewCell 的awakeFromNib 函数中添加 UISwipeGestureRecognizer,因此它只添加一次。
UICollectionViewCell.m
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?
至于退出删除模式,我创建了一个自定义 UIGestureRecognizer 和 UIViews 的 NSArray。我从这个问题中借用了@iMS 的想法:UITapGestureRecognizer - 让它在触地时工作,而不是触地?
On touchesBegan, if the touch point isn't within any of the UIViews, the gesture succeeds and delete mode is exited.
在 touchesBegan 上,如果触摸点不在任何 UIViews 内,则手势成功并退出删除模式。
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.
通过这种方式,我可以将单元格(和任何其他视图)中的删除按钮传递给 UIGestureRecognizer,如果触摸点在按钮的框架内,删除模式将不会退出。
TouchDownExcludingViewsGestureRecognizer.h
TouchDownExclusionViewsGestureRecognizer.h
#import <UIKit/UIKit.h>
@interface TouchDownExcludingViewsGestureRecognizer : UIGestureRecognizer
@property (nonatomic) NSArray *excludeViews;
@end
TouchDownExcludingViewsGestureRecognizer.m
TouchDownExclusionViewsGestureRecognizer.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):
实现(在包含 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
}
回答by Jacek Lampart
You can try adding a UISwipeGestureRecognizer to each collection cell, like this:
您可以尝试向每个集合单元添加 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];
}
followed by:
其次是:
- (void)userDidSwipe:(UIGestureRecognizer *)gestureRecognizer {
if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
//handle the gesture appropriately
}
}
回答by Reynaldo Aguilar
There is a more standard solution to implement this feature, having a behavior very similar to the one provided by UITableView
.
有一种更标准的解决方案来实现此功能,其行为与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:
为此,您将使用 aUIScrollView
作为单元格的根视图,然后将单元格内容和删除按钮放置在滚动视图内。单元格类中的代码应该是这样的:
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:
在这段代码中,我们将属性isPagingEnabled
设置true
为使滚动视图仅在其内容的边界处停止滚动。这个单元格的布局子视图应该是这样的:
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:
为了解决这个问题,我们只需要指示滚动视图忽略由它而不是由它的子视图之一处理的触摸事件。要实现这一点,只需创建一个子类UIScrollView
并覆盖以下函数:
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
.
现在在您的单元格中,您应该使用这个新子类的实例而不是标准的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 . Since we are ignoring touches that are handled directly by the scroll view, then its pan gesture recognizer won't be able to start recognizing touch events. However, this can be easily fixed by indicating to the scroll view that its pan gesture recognizer will be handled by the cell and not by the scroll. You do this adding the following line at the bottom of your cell's init(frame: CGRect)
:
如果您现在运行该应用程序,您将看到我们重新选择了单元格,但这次滑动不起作用。由于我们忽略了由滚动视图直接处理的触摸,因此它的平移手势识别器将无法开始识别触摸事件。但是,这可以通过向滚动视图指示其平移手势识别器将由单元格而不是由滚动处理来轻松解决。您可以在单元格的底部添加以下行init(frame: CGRect)
:
addGestureRecognizer(scrollView.panGestureRecognizer)
This may look like a bit hacky, but it isn't. By design, the view that contains a gesture recognizer and the target of that recognizer don't have to be the same object.
这可能看起来有点 hacky,但事实并非如此。根据设计,包含手势识别器的视图和该识别器的目标不必是同一个对象。
After this change all should be working as expected. You can see a full implementation of this idea in this repo
在此更改后,所有内容都应按预期工作。你可以在这个 repo 中看到这个想法的完整实现