I want to add a gesture that will only fire if a person has been pressing for a second or so. Not a tap but a long press. If I use uilongpressgesturerecognizer it keeps firing until I release my finger. How can I get around this.
Set minimumPressDuration when you create and add gesture as below:
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc]
initWithTarget:self action:#selector(handleLongPress:)];
longPress.minimumPressDuration = 1.0;
Write your code in UIGestureRecognizerStateEnded state as below:
-(void)handleLongPress:(UILongPressGestureRecognizer *)Gesture{
if (Gesture.state == UIGestureRecognizerStateEnded) {
//Do any thing after long press ended,which will be 1.0 second as set above
}
else if (Gesture.state == UIGestureRecognizerStateBegan){
}
}
Swift 5
Declare a UILongPressGestureRecognizer:
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(gestureAction(gesture:)))
Set its .minimumPressDuration to 1.0 or any interval you want.
Set the recognizers .delegate to your ViewController and add it to your view using .addGestureRecognizer().
Use the following function to handle the gesture:
#objc func gestureAction(gesture: UIGestureRecognizer) {
if let longPress = gesture as? UILongPressGestureRecognizer {
if longPress.state == UIGestureRecognizer.State.began {
} else {
}
}
}
Set value of minimumPressDuration property of UILongPressGestureRecognizer.
I have a custom UITableView cell and I want to add a long press gesture recognizer to it. Currently, I'm doing it as so:
longPressGesture.minimumPressDuration = 1.0
longPressGesture.addTarget(self, action: "testFeedback")
cell.addGestureRecognizer(longPressGesture)
I'm doing it programmatically because I could not find a good way to detect which cell was tapped within an IBAction. However, I'm having a hard time getting this to work I want to pass a parameter through the selector. I am not opposed to doing this in storyboards, but would appreciate some guidance on it.
Thanks!
testFeedback function should look like this
func testFeedback(gestureRecognizer:UIGestureRecognizer) {
if (gestureRecognizer.state == UIGestureRecognizerState.Ended) {
var point = gestureRecognizer.locationInView(self.tableView)
if let indexPath = self.tableView.indexPathForRowAtPoint(point)
{
println(indexPath.row) /// long press ended
}
}
else if (gestureRecognizer.state == UIGestureRecognizerState.Began){
/// long press started
}
}
I have a UITextField that has User Interaction Disabled. So if you tap on this text field, nothing happens. Normally to check if a text field was tapped Id try the delegate methods, but I cannot because user interaction is disabled. Is there any way I can check if the text field was tapped/touched? I change another element to hidden = no; when it is tapped so I was wondering if its even possible enabling user interaction.
Best option is to turn on User Interaction and disable edit action using delegate method.
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField
{
return NO;
}
You can call your method inside that function to detect tap.
Maybe, you can add UITapGestureRecognizer in the superview, detect if the touch is inside the frame, and then do something.
Detect touch if it is inside the frame of the super view
Create UITapGestureRecognizer and add that to the UITextField's super view.
Implement the target selector and check if the gesture's state has ended.
Call your method.
Objective-C
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(didRecognizeTapGesture:)];
[self.textField.superview addGestureRecognizer:tapGesture];
- (void) didRecognizeTapGesture:(UITapGestureRecognizer*) gesture {
CGPoint point = [gesture locationInView:gesture.view];
if (gesture.state == UIGestureRecognizerStateEnded) {
if (CGRectContainsPoint(self.textField.frame, point)) {
[self doSomething];
}
}
}
Swift 3
func viewDidLoad() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didRecognizeTapGesture(_:)))
textField.superView?.addGestureRecognizer(tapGesture)
}
private dynamic func didRecognizeTapGesture(_ gesture: UITapGestureRecognizer) {
let point = gesture.location(in: gesture.view)
guard gesture.state == .ended, textField.frame.contains(point) else { return }
//doSomething()
}
I'm trying to implement a UIPanGestureRecognizer in my UITableViewController to use for a swipe to delete animation. Similar to the swipe to delete used in the Clear app, where if you swipe a UITableViewCell in left or right the cell moves and gets deleted.
I have tried implementing this in my UITableViewCell subclass but it never seems to receive the event.
This is the code I put in my UITableViewCell subclass to try this functionality. In my init method
UIGestureRecognizer *recognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
recognizer.delegate = self;
[self addGestureRecognizer:recognizer];
and then the methods to handle it:
- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer {
CGPoint translation = [gestureRecognizer translationInView:self.superview];
//might have to change view to tableView
//check for the horizontal gesture
if (fabsf(translation.x) > fabsf(translation.y)) {
return YES;
NSLog(#"Panning");
}
return NO;
}
- (void)handlePan:(UIPanGestureRecognizer *)recognizer {
if (recognizer.state == UIGestureRecognizerStateBegan) {
//if the gesture has just started record the center location
NSLog(#"handlePan");
_originalCenter = self.center; //Declared as a CGPoint at the top of my TableViewCell
}
if (recognizer.state == UIGestureRecognizerStateChanged) {
//translate the center (aka translate from the center of the cell)
CGPoint translation = [recognizer translationInView:self];
self.center = CGPointMake(_originalCenter.x + translation.x, _originalCenter.y);
// determine whether the item has been dragged far enough to delete/complete
}
if (recognizer.state == UIGestureRecognizerStateEnded) {
// the frame this cell would have had before being dragged
CGRect originalFrame = CGRectMake(0, self.frame.origin.y, self.bounds.origin.x, self.bounds.size.height);
[UIView animateWithDuration:0.2 animations:^{
self.frame = originalFrame;}
];
}
}
The Cells don't move at all though. Not really sure what's going on here
If you don't want the cell's swipe gesture to happen simultaneously with the table view scroll gesture, then add a pan gesture to your cell and make it a delegate:
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(doPan:)];
pan.delegate = self;
[self addGestureRecognizer:pan];
And implement the following delegate method to only start if the pan is horizontal:
#pragma mark - UIGestureRecognizerDelegate
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
// note: we might be called from an internal UITableViewCell long press gesture
if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
UIPanGestureRecognizer *panGestureRecognizer = (UIPanGestureRecognizer*)gestureRecognizer;
UIView *cell = [panGestureRecognizer view];
CGPoint translation = [panGestureRecognizer translationInView:[cell superview]];
// Check for horizontal gesture
if (fabs(translation.x) > fabs(translation.y))
{
return YES;
}
}
return NO;
}
Swift3 ..
override func awakeFromNib() {
super.awakeFromNib()
// do not use, say, layoutSubviews as layoutSubviews is called often
let p = UIPanGestureRecognizer(target: self, action: #selector(yourPan))
p.delegate = self
contentView.addGestureRecognizer(p)
}
}
override func gestureRecognizerShouldBegin(_ g: UIGestureRecognizer) -> Bool {
if (g.isKind(of: UIPanGestureRecognizer.self)) {
let t = (g as! UIPanGestureRecognizer).translation(in: contentView)
let verticalness = abs(t.y)
if (verticalness > 0) {
print("ignore vertical motion in the pan ...")
print("the event engine will >pass on the gesture< to the scroll view")
return false
}
}
return true
}
You need the following method in order for the gesture to be detected in sync with the scrollView's panGesture:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES; //otherGestureRecognizer is your custom pan gesture
}
Remember to set the panGesture.delegate to your viewController. (Updated with OlivaresF's comment.)
Add the gesture recognizer to the content view.
[self.contentView addGestureRecognizer:recognizer];
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