Constrain movement of cell in UICollectionview to bounds of the CollectionView - ios

I'm getting some weird behavior (actually a crash) when I drag a cell outside of the CollectionView Bounds. How can I limit the user interaction of the CollectionView to only the frame of the CollectionView? When the user long-presses and drags a view cell, I only want them to be able to move the cell within the CollectionView, and not all over the screen
private void HandleLongPressOnCollection(UILongPressGestureRecognizer gesture)
{
switch (gesture.State)
{
case UIGestureRecognizerState.Began:
var myIndexPath = MyCollectionView.IndexPathForItemAtPoint(gesture.LocationInView(MyCollectionView));
this.selectedIndexPath = myIndexPath;
BeginInteractiveMovementForItem();
break;
case UIGestureRecognizerState.Changed: // This is not working correctly
if (MyCollectionView.Frame.Contains(gesture.LocationInView(MyCollectionView)))
{
MyCollectionView.UpdateInteractiveMovement(gesture.LocationInView(MyCollectionView));
}
break;
case UIGestureRecognizerState.Ended:
EndInteractiveMovementForItem();
break;
default:
MyCollectionView.CancelInteractiveMovement();
break;
}
}
private void BeginInteractiveMovementForItem()
{
if (selectedIndexPath != null)
{
MyCollectionView.BeginInteractiveMovementForItem(selectedIndexPath);
var cell = MyCollectionView.CellForItem(selectedIndexPath) as CustomViewCell;
cell.MarkCellAsMoving();
}
}
private void EndInteractiveMovementForItem()
{
if (selectedIndexPath != null)
{
var cell = MyCollectionView.CellForItem(selectedIndexPath) as CustomViewCell;
cell?.SetToNormalState();
}
MyCollectionView.EndInteractiveMovement();
}

You should compare the gesture's location with ContentView not Framelike:
case UIGestureRecognizerState.Changed:
CGPoint gesturePoint = gesture.LocationInView(MyCollectionView);
CGSize contentSize = MyCollectionView.ContentSize;
if (gesturePoint.X > 0 && gesturePoint.X < contentSize.Width &&
gesturePoint.Y > 0 && gesturePoint.Y < contentSize.Height)
{
MyCollectionView.UpdateInteractiveMovement(gesture.LocationInView(MyCollectionView));
}
break;
Adjust the constant in the if statement to feed your requirement.
Moreover the crash may occur in the event MoveItem() when you call MyCollectionView.EndInteractiveMovement();. You can try to post some code about it.

You can use
collectionView.bounces = false
if you want to drag until the edge.
Can you also post the crash you are getting?

Related

how to get indexpath of tableView in any method

I'm trying to show animation if the indexPath == 0 and scroll direction is downward, but I'm unable to get the indexPath of tableView in scrollViewdidScroll method. here's my code -->
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let indexPath = tableView.indexPath(for: ThirdTableViewCell() as UITableViewCell)
if (self.lastContentOffset > scrollView.contentOffset.y) && indexPath?.row == 0 {
print ("up")
}
else if (self.lastContentOffset < scrollView.contentOffset.y) {
// print ("down")
}
self.lastContentOffset = scrollView.contentOffset.y
}
I got the indexPath nil while debugging
You still haven't explained what you really want to do, but maybe this will help...
Start by setting up your scrollViewDidScroll like this:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
var partOfRowZeroIsVisible: Bool = false
var topOfRowZeroIsVisible: Bool = false
var userIsDraggingDown: Bool = false
// if user pulls table DOWN when Row Zero is at top,
// contentOffset.y will be negative and table will
// bounce back up...
// so, we make y AT LEAST 0.0
let y = max(0.0, tableView.contentOffset.y)
// point for the top visible content
let pt: CGPoint = CGPoint(x: 0, y: y)
if let idx = tableView.indexPathForRow(at: pt), idx.row == 0 {
// At least PART of Row Zero is visible...
partOfRowZeroIsVisible = true
} else {
// Row Zero is NOT visible...
partOfRowZeroIsVisible = false
}
if y == 0 {
// Table view is scrolled all the way to the top...
topOfRowZeroIsVisible = true
} else {
// Table view is NOT scrolled all the way to the top...
topOfRowZeroIsVisible = false
}
if (self.lastContentOffset >= y) {
// User is DRAGGING the table DOWN
userIsDraggingDown = true
} else {
// User is DRAGGING the table UP
userIsDraggingDown = false
}
// save current offset
self.lastContentOffset = y
Then, to show or hide your "arrow":
// if you want to show your "arrow"
// when ANY PART of Row Zero is visible
if partOfRowZeroIsVisible {
arrow.isHidden = false
} else {
arrow.isHidden = true
}
or:
// if you want to show your "arrow"
// ONLY when table view has been scrolled ALL THE WAY to the top
if topOfRowZeroIsVisible {
arrow.isHidden = false
} else {
arrow.isHidden = true
}
or:
// if you want to show your "arrow"
// when ANY PART of Row Zero is visible
// and
// ONLY if the user is dragging DOWN
if partOfRowZeroIsVisible && userIsDraggingDown {
arrow.isHidden = false
} else {
arrow.isHidden = true
}
or:
// if you want to show your "arrow"
// ONLY when table view has been scrolled ALL THE WAY to the top
// and
// ONLY if the user is dragging DOWN
if topOfRowZeroIsVisible && userIsDraggingDown {
arrow.isHidden = false
} else {
arrow.isHidden = true
}

Enlarge UICollectionViewCell when long press and dragged

I want to scale the collectionview when longpress and dragged and when user end drag then cell should come in regular size.
I am creating demo using below steps which works fine but enlarge is not working as I expected.
Collection View Example
Here is the gesture code I used :
func handleLongGesture(gesture: UILongPressGestureRecognizer) {
switch(gesture.state) {
case UIGestureRecognizerState.Began:
guard let selectedIndexPath = self.collectionView.indexPathForItemAtPoint(gesture.locationInView(self.collectionView)) else {
break
}
collectionView.beginInteractiveMovementForItemAtIndexPath(selectedIndexPath)
case UIGestureRecognizerState.Changed:
collectionView.updateInteractiveMovementTargetPosition(gesture.locationInView(gesture.view!))
case UIGestureRecognizerState.Ended:
collectionView.endInteractiveMovement()
default:
collectionView.cancelInteractiveMovement()
}
}
Try this on a collection view layout sub-class:
- (UICollectionViewLayoutAttributes*) layoutAttributesForInteractivelyMovingItemAtIndexPath:(NSIndexPath *)indexPath withTargetPosition:(CGPoint)position
{
UICollectionViewLayoutAttributes *attributes = [super layoutAttributesForInteractivelyMovingItemAtIndexPath:indexPath withTargetPosition:position];
attributes.zIndex = NSIntegerMax;
attributes.transform3D = CATransform3DScale(attributes.transform3D, 1.2f, 1.2f, 1.0);
return attributes;
}

Swift how to move UIButton programmatically?

I have an infoButton UIButton outlet in my swift file. This button is originally placed in the storyboard with the following constraints:
I want to move this button down by 50 pixels depending if NoAds boolean is true or false.
I have tried doing this, but I can't seem to move it.
if NoAds{
print("No Ads")
infoButton.center.y = infoButton.center.y - 50
}else{
print("Ads")
loadBanner()
}
I assume this should be an easy fix?
EDIT: The ad is a standard google ad banner which is 320 wide and 50 high.
have an outlet for the top constraint
change the topConstraint.constant
like this:
if NoAds{
print("No Ads")
//topConstraint.constant -= 50
topConstraint.constant = 50 // Avoid using -= or += if this func will call a lot of time
}else{
print("Ads")
// set the default constant you want
topConstraint.constant = 100
loadBanner()
}
Use an IBOutlet to connect to the constraint as #wilson-xj describes.
Then use the following code to animate the change to the constraint. This will add some polish and allow the ad to slide in/out nicely instead of jarring the user by instantly shifting content they may be interacting with.
view.layoutIfNeeded()
UIView.animateWithDuration(0.30) { () -> Void in
self.topConstraint.constant = constraintConstant // 50 or 100 etc
self.view.layoutIfNeeded()
}
The best way would probably be to add the constraints and dynamically change the button, but if you didn't want to add another outlet and wanted to do this dynamically you could change the position of the button frame like so:
var X_Position:CGFloat? = startButton.frame.origin.x + 50 //add 50 to move button down page
var Y_Position:CGFloat? = startButton.frame.origin.y
startButton.frame = CGRectMake(X_Position, Y_Position, startButton.frame.width, startButton.frame.height)
This will redraw the button in the new position.
Try this i hope it would be helpful This is mine code i am using!!
and i Assume that you all set the constraints like Top,Bottom,leading,Trailing space
if NoAds
{
print("No Ads")
for item in self.view.constraints
{
if item.firstItem .isKindOfClass(UIButton)
{
let newField = item.firstItem as! UIButton
if newField == buttonName && item.firstAttribute == NSLayoutAttribute.Top
{
item.constant = -50
self.view .layoutIfNeeded()
}
}
}
}else{
print("Ads")
for item in self.view.constraints
{
if item.firstItem .isKindOfClass(UIButton)
{
let newField = item.firstItem as! UIButton
if newField == buttonName && item.firstAttribute == NSLayoutAttribute.Top
{
item.constant = 0
self.view .layoutIfNeeded()
}
}
}
loadBanner()
}

How can I reload UITableView when I scroll to the top with current scroll position? (like chat apps)

I'm working on a chat screen now(UITableView + input TextField).
I want my UITableView reload more chat messages staying current scroll position when I scroll to top(If I load older 20 messages more, I still seeing a 21st message.) KakaoTalk and Line Apps is doing this. In that apps, I can scroll up infinitely because the scroll position is staying.(I mean I'm still seeing the same chat message)
I'm checking the row index in tableView:willDisplayCell:forRowAtIndexPath:, so I fetch more chat messages when the index is 0. I finished the logic fetching more messages from DB, but I didn't finish polishing UI.
This is the code.
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.row == 0 && !isTableViewLoading {
if photo.chatMessages.count != chatMessages.count {
var loadingChatItems: Array<ChatMessage>
let start = photo.chatMessages.count - chatMessages.count - Metric.ChatMessageFetchAmount
if start > 0 {
loadingChatItems = Database.loadChatMessages(photoId, startIndex: start, amount: Metric.ChatMessageFetchAmount)
} else {
loadingChatItems = Database.loadChatMessages(photoId, startIndex: 0, amount: Metric.ChatMessageFetchAmount + start)
}
chatMessages = loadingChatItems + chatMessages
isTableViewLoading = true
var indexPaths = Array<NSIndexPath>()
for row in 0..<loadingChatItems.count {
let indexPath = NSIndexPath(forRow: row, inSection: 0)
indexPaths.append(indexPath)
}
tableView.scrollEnabled = false
tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.None)
}
} else {
isTableViewLoading = false
tableView.scrollEnabled = true
}
}
Any suggestions?
Swift 3
This post is a bit old, but here a solution:
1) Detect when you are scrolling near to the top in scrollViewDidScroll,
2) Load the new messages
3) Save the contentSize, reload the tableView
4) Set the contentOffset.y of the tableView to (newContentSizeHeight - oldContentSizeHeight) which is exactly the current point
and here the code:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == tableView {
if tableView.contentOffset.y < 50 {
loadMessages()
let oldContentSizeHeight = tableView.contentSize.height
tableView.reloadData()
let newContentSizeHeight = tableView.contentSize.height
tableView.contentOffset = CGPoint(x:tableView.contentOffset.x,y:newContentSizeHeight - oldContentSizeHeight)
}
}
}
In viewDidLoad:
self.refreshControl?.addTarget(self, action: "refresh:", forControlEvents: UIControlEvents.ValueChanged)
Then add the function:
func refresh(refreshControl: UIRefreshControl) {
//your code here
refreshControl.endRefreshing()
}
This will allow the user to scroll up to refresh the UITableView.
I have implemented another solution, it uses the scroll listener from the delegate, and the response of the asynchronous service.
So, it scrolls without animation to the position where the loading of more messages started, because messages are being appended to my array (It causes the transition to be barely affected).
I decided to do it in this way because the first solution has a problem, and it is that when you let your row height to be set automatically, then the new content height will not be accurate.
Here's my code.
- (void) scrollViewDidScroll:(UIScrollView*)scrollView {
if (!topReached && scrollView == self.table) {
if (!isLoadingTop && self.table.contentOffset.y < 40) {
isLoadingTop = YES;
[self loadChat];
}
}
}
// method used by the response of the async service
- (void)fillMessages: (NSArray*) messageArray {
if ([messageArray count] == 0) {
topReached = YES;
} else {
if ([messageArray count] < limit) {
topReached = YES;
}
for (int i=0; i<messageArray.count; i++) {
NSDictionary* chat = [messageArray objectAtIndex:i];
[messages insertObject:[[MessageModel alloc] initWithDictionary:chat] atIndex:i];
}
[table reloadData];
if (!isLoadingTop)
[self scrollToBottom];
else {
isLoadingTop = NO;
[self.table
scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:[messageArray count] inSection:0]
atScrollPosition:UITableViewScrollPositionTop animated:false];
}
offset += limit;
}
}
As an adicional information, I like to block the access of my async services consumptions in a view controller, so one service can be called just once in the same view.

How to implement UITableView`s swipe to delete for UICollectionView

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.
Also, I am using PSTCollectionView wrapper to support 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.
There is a simpler solution to your problem that avoids using gesture recognizers. The solution is based on UIScrollView in combination with 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. The stackView will act as a content view. Make sure that the views have equal widths with stackView.distribution = .fillEqually.
You’ll embed the stackView inside a UIScrollView that has paging enabled. The scrollView should be constrained to the edges of the cell. Then you’ll set the stackView’s width to be 2 times the scrollView’s width so each of the container views will have the width of the cell.
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.
Its very simple..You need to add a customContentView and customBackgroundView behind the customContentView.
After that and you need to shift the customContentViewto the left as user swipes from right to left. Shifting the view makes visible to the customBackgroundView.
Lets Code:
First of all you need to add panGesture to your 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 here in Swift 3.
It is a modified version of this tutorial.
In the Collection View Programming Guide for iOS, in the section Incorporating Gesture Support, the docs read:
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.
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.
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?
On touchesBegan, if the touch point isn't within any of the UIViews, the gesture succeeds and delete mode is exited.
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.
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
Implementation (in the UIViewController containing UICollectionView):
#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
}
You can try adding a UISwipeGestureRecognizer to each collection cell, like this:
-(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
}
}
With iOS 14, you can use UICollectionViewLayoutListConfiguration in conjunction with UICollectionViewCompositionalLayout to get this functionality natively for free, no custom cells or gesture recognizes needed.
If your minimum deploy target is >= iOS 14.x, this is probably the preferred method from now on, and it will also let you to adopt modern cell configuration with UIContentView and UIContentConfiguration to boot.
There is a more standard solution to implement this feature, having a behavior very similar to the one provided by 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:
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:
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:
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.
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):
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.
After this change all should be working as expected. You can see a full implementation of this idea in this repo

Resources