我想问一下如何在UICollectionView中实现UITableView滑动删除的相同行为。我正在尝试找到一个教程,但我找不到任何教程。
另外,我使用PSTCollectionView包装器来支持iOS 5.
谢谢!
编辑: 滑动识别器已经很好了。 我现在需要的是取消删除模式时与UITableView相同的功能,例如:当用户点击表格视图中的单元格或空白区域时(即,当用户点击“删除”按钮之外)时。 UITapGestureRecognizer不起作用,因为它只检测触摸释放时的点击。 UITableView在手势开始时检测到触摸(而不是在释放时),并立即取消删除模式。
答案 0 :(得分:13)
在适用于iOS的集合视图编程指南中,在Incorporating Gesture Support部分中,文档为:
您应始终将手势识别器附加到集合视图本身,而不是附加到特定单元格或视图。
所以,我认为向UICollectionViewCell
添加识别器并不是一个好习惯。
答案 1 :(得分:11)
非常简单..您需要在customContentView
后面添加customBackgroundView
和customContentView
。
之后,当用户从右向左滑动时,您需要将customContentView
向左移动。移动视图使customBackgroundView
可见。
让代码:
首先,您需要将panGesture添加到UICollectionView
override func viewDidLoad() {
super.viewDidLoad()
self.panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.panThisCell))
panGesture.delegate = self
self.collectionView.addGestureRecognizer(panGesture)
}
现在将选择器实现为
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")
}
}
更新约束的辅助方法
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() }
})
}
我在Swift 3中创建了一个示例项目here。
这是此tutorial的修改版本。
答案 2 :(得分:7)
有一个更简单的解决方案,可以避免使用手势识别器。该解决方案基于UIScrollView
与UIStackView
的结合。
首先,您需要创建2个容器视图-一个用于单元格的可见部分,一个用于隐藏部分。您将这些视图添加到UIStackView
中。 stackView
将充当内容视图。确保视图的宽度与stackView.distribution = .fillEqually
相等。
您将把stackView
嵌入到启用分页的UIScrollView
内。 scrollView
应该限制在单元格的边缘。然后,您将stackView
的宽度设置为scrollView
宽度的2倍,以便每个容器视图都具有单元格的宽度。
通过此简单的实现,您已经创建了具有可见和隐藏视图的基本单元。使用可见视图将内容添加到单元格,在隐藏视图中可以添加删除按钮。这样您就可以实现:
我已经设置了example project on GitHub。您也可以read more about this solution here。
该解决方案的最大优点是操作简便,无需处理约束和手势识别器。
答案 3 :(得分:3)
我对@JacekLampart采用了类似的方法,但决定在UICollectionViewCell的awakeFromNib函数中添加UISwipeGestureRecognizer,因此只添加一次。
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
}
}
至于退出删除模式,我使用UIViews的NSArray创建了一个自定义UIGestureRecognizer。我从这个问题中借用了@iMS的想法:UITapGestureRecognizer - make it work on touch down, not touch up?
在touchesBegan上,如果触摸点不在任何UIViews内,则手势成功并退出删除模式。
通过这种方式,我可以将单元格(以及任何其他视图)中的删除按钮传递给UIGestureRecognizer,如果触摸点位于按钮的框架内,则删除模式不会退出。
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
实现(在包含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
}
答案 4 :(得分:2)
您可以尝试将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];
}
接下来是:
- (void)userDidSwipe:(UIGestureRecognizer *)gestureRecognizer {
if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
//handle the gesture appropriately
}
}
答案 5 :(得分:1)
在 iOS 14 中,您可以将 UICollectionViewLayoutListConfiguration
与 UICollectionViewCompositionalLayout
结合使用以在本机免费获得此功能,无需自定义单元格或手势识别。
如果您的最低部署目标是 >= iOS 14.x,这可能是从现在开始的首选方法,并且它还可以让您采用带有 UIContentView
和 UIContentConfiguration
的现代单元配置启动。
答案 6 :(得分:0)
有一个更标准的解决方案来实现此功能,其行为与UITableView
提供的行为非常相似。
为此,您将使用UIScrollView
作为单元格的根视图,然后将单元格内容和删除按钮放置在滚动视图中。您的单元格类中的代码应如下所示:
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(scrollView)
scrollView.addSubview(viewWithCellContent)
scrollView.addSubview(deleteButton)
scrollView.isPagingEnabled = true
scrollView.showsHorizontalScrollIndicator = false
}
在此代码中,我们将属性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)
}
使用此代码后,如果您运行该应用程序,将会看到要删除的滑动按预期工作,但是,我们失去了选择单元格的能力。问题在于,由于滚动视图填充了整个单元格,因此所有触摸事件都由它处理,因此集合视图将永远没有机会选择该单元格(这类似于在单元格中有一个按钮时,因为触摸该按钮不会触发选择过程,而是由按钮直接处理。)
要解决此问题,我们只需要指示滚动视图即可忽略由它而不是由其子视图之一处理的触摸事件。为此,只需创建UIScrollView
的子类并覆盖以下函数:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
return result != self ? result : nil
}
现在在您的单元格中,您应该使用这个新子类的实例,而不是标准的UIScrollView
。
如果现在运行该应用程序,您将看到我们已经选择了单元格,但是这次滑动不起作用。由于我们忽略了滚动视图直接处理的触摸,因此其平移手势识别器将无法开始识别触摸事件。但是,通过向滚动视图指示其平移手势识别器将由单元而不是滚动来处理,可以轻松解决此问题。为此,请在单元格init(frame: CGRect)
的底部添加以下行:
addGestureRecognizer(scrollView.panGestureRecognizer)
这看起来有点黑,但事实并非如此。根据设计,包含手势识别器的视图和该识别器的目标不必是同一对象。
此更改后,所有功能均应按预期工作。您可以看到此想法in this repo
的完整实施