When I add a UILongPressGestureRecognizer on my collectionView (I'm doing it like in this answer), the collection view stops highlighting cells as expected.
I'm handling the cell highlighting in my UICollectionViewCellSubclass, like it's explained in this answer:
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
if (self.highlighted) {
//Here I show my highlighting layer
}
else {
//Here I hide my highlighting layer
}
}
- (void)setHighlighted:(BOOL)highlighted
{
[super setHighlighted:highlighted];
[self setNeedsDisplay]; //This call draw rect and show/hide the highlighting layer
}
The problem seems to be that, when I add the UILongPressGestureRecognizer gesture on the collection view, drawRect is not being called after setting highlighted to YES.
If I move the logic to show my highlighting layer in setHighlighted:YES, I can't see the touching-feedback because setHighlighted:NO its being called immediately after! (This happens only after adding the UILongPressGestureRecognizer).
I've also tried to move the logic in touchesBegan/touchesEnded but then didSelectRowAtIndexPath: doesn't work anymore.
Unfortunately, it looks like the UILongPressGestureRecognizer is blocking the touches required for the UICollectionView to highlight the cell. It doesn't look like there is a good way to forward the touches, so I'd imagine you will have to handle the highlighting manually if you want that to appear. You could do it through a separate method than setHighlighted, or try to use the cell.highlighted property to do your drawing.
Here is an example I came up with that worked for my UICollectionView
-(void)handleLongPress:(UILongPressGestureRecognizer *)gestureRecognizer
{
// if (gestureRecognizer.state != UIGestureRecognizerStateEnded) {
// return;
// }
CGPoint p = [gestureRecognizer locationInView:self.collectionView];
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:p];
if (indexPath == nil){
NSLog(#"couldn't find index path");
} else {
// get the cell at indexPath (the one you long pressed)
UICollectionViewCell* cell = [self.collectionView cellForItemAtIndexPath:indexPath];
if (gestureRecognizer.state != UIGestureRecognizerStateEnded) {
if (!cell.highlighted) {
[cell setHighlighted:YES];
}
return;
}
[cell setHighlighted:NO];
// do stuff with the cell
}
}
Related
I've UITableView and its cell have one UITextField.
Now I added long gesture in UITextField but it not working. When I tap long gesture on textfield it always show context menu (select,copy cut,past,etc.).
My question is that how to manage long gesture as well as context menu in UITextFiled.
I've tried below code:
longGesture = [[UILongPressGestureRecognizer alloc]
initWithTarget:self action:#selector(handleLongPress:)];
longGesture.minimumPressDuration = 2.0; //seconds
longGesture.delegate = self;
-(void)handleLongPress:(UILongPressGestureRecognizer *)gestureRecognizer
{
CGPoint p = [gestureRecognizer locationInView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:p];
if (indexPath == nil) {
NSLog(#"long press on table view but not on a row");
} else if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
NSLog(#"long press on table view at row %ld", indexPath.row);
} else {
NSLog(#"gestureRecognizer.state = %ld", gestureRecognizer.state);
}
}
Tableview delegate method
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
Note *obj = [self.dataArr objectAtIndex:indexPath.row];
TableViewCell *Cell = [self.tableView dequeueReusableCellWithIdentifier:#"cell"];
if (Cell == nil) {
Cell = [[TableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"cell"];
Cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
else{
Cell.memoField.text = obj.memoRowText;
}
Cell.memoField.userInteractionEnabled = YES;
[Cell.memoField addGestureRecognizer:longGesture];
Cell.memoField.delegate = self;
Cell.memoField.tag = indexPath.row;
return Cell;
}
You'll want to set up a failure requirement between the gesture that shows the context menu and your long press gesture. Specifically, you'll want the menu recognizer to require your long press to fail (i.e. you'll want the menu recognizer to wait until it has ruled out the long press). In code, one way to do that is this is implementing this delegate method.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)longPress shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)other {
if (other.view /* is a text field in the table view */) {
return YES;
} else {
return NO;
}
}
These methods can be a little confusing.
Remember that you can add "static" failure requirements with -[UIGestureRecognizer requireGestureRecognizerToFail:], but in many cases you don't necessary easily have references to both recognizers (such as in this case).
In many cases, this suffices.
However, the gesture recognizer system also gives you a chance to install failure requirements "on the fly".
Returning YES from -gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer: has the same effect as if you called [second requireFailureOfGestureRecognizer:first] (where first and second are the first and second arguments to that method).
OTOH returning YES from -gestureRecognizer:shouldRequireFailureOfGestureRecognizer: has the same effect as if you called [first requireFailureOfGestureRecognizer:second].
I have an UITableViewController that contains a tableView. I have added a tap gesture recognizer to this controller and I would like the action to happen when the user clicks everywhere except for the first row of the tableView (with indexPath.row == 0).
I have seen the following related questions and answers (among others):
this one is concerned with detecting the tableView itself (therefore it will also detect its header / footer).
I has used this one (code below) before but the problem is that it computes an indexPath of (0,0) also if one clicks on the footer / header.
.
-(BOOL) gestureRecognizer:(UIGestureRecognizer *)gesture shouldReceiveTouch:(UITouch *)touch {
CGPoint buttonPosition = [touch locationInView:self.view];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:buttonPosition];
if (indexPath.row == 0) {
return NO; // will also be returned if I click on the header / footer
}
return YES;
}
Perhaps attempt to add an "invisible" UIButton over the table view cells and then upon tap use the buttons tag as your index. Like the pseudo code below:
-cellForRowAtIndexPath:index {
UIButton *button = [UIButton alloc]initWithFrame:cell.frame];
[button setTag:index.row];
[button addTarget:self action:#selector(getIndex:) forControlEvents:UIControlEventTouchUpInside];
[cell addSubview: button]
}
-(void)getIndex:(UIButton *)sender {
finalIndex = [sender tag];
}
I am programmatically adding a UIButton to a custom UICollectionViewCell. Taps on the button are only detected if I do a long press on the cell first. Then the button works as expected. I have to do the same long press on each cell individually in order to get their respective button to recognize taps.
Note that single taps (i.e. didSelectItemAtIndexPath) on cells work as expected. So does long press on a cell. It might be worth noting that UILongPressGestureRecognizer is the only gesture added to the collectionView.
UICollectionViewCell class
- (UIButton *)stackInfoButton
{
if(_stackInfoButton == nil)
{
self.stackInfoButton = [UIButton buttonWithType:UIButtonTypeInfoDark];
[self.stackInfoButton setShowsTouchWhenHighlighted:YES];
}
return _stackInfoButton;
}
- (void)layoutSubviews
{
// Other stuff here. Removed for readability
self.stackInfoFrame = CGRectMake(4.0f, self.bounds.size.height - self.stackInfoButton.bounds.size.height - 4, self.stackInfoButton.bounds.size.width, self.stackInfoButton.bounds.size.height);
}
- (void)drawRect:(CGRect)rect
{
// Other stuff here. Removed for readability
self.stackInfoButton.frame = self.stackInfoFrame;
[[self contentView] addSubview:self.stackInfoButton];
}
UICollectionViewController class
- (void)viewDidLoad
{
[super viewDidLoad];
// Other stuff here. removed for readability
self.longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPress:)];
self.longPressGesture.delegate = self;
[self.collectionView addGestureRecognizer:self.longPressGesture];
self.longPressGesture.cancelsTouchesInView = NO;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
DVCardViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kDVCollectionViewCellReuseIdentifier forIndexPath:indexPath];
cell.indexPath = indexPath;
// Other stuff here. Removed for readability
[cell.stackInfoButton addTarget:self action:#selector(stackInfoButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
return cell;
}
- (void)stackInfoButtonTapped:(id)sender
{
NSLog(#">>> Entering %s <<<", __PRETTY_FUNCTION__);
}
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
NSLog (#"Touch View : %#", touch.view);
// tried both.
// DVCardViewCell *cell;
// if (touch.view == cell.stackInfoButton)
if ( [touch.view isKindOfClass:[UIButton class]] )
{
NSLog(#"Gesture on button, returns no");
return NO;
}
NSLog(#"Gesture returns outside, yes");
return YES;
}
Suggestions appreciated. I've been through many UICollectionView threads on SO. Yet to find one that addresses my issue. Thanks.
You say you have added a gesture to the collection view, so you need to teach that gesture not to steal touches that it shouldn't get involved with. Add your controller as the delegate of the gesture and implement gestureRecognizer:shouldReceiveTouch:. In that method check the view that the touch hit and, if it's a button, return NO to prevent the gesture from getting involved.
Try this
self.LongPressGesture.cancelsTouchesInView = NO;
Sorry was mistaken. never mind.
This looks really weird:
- (UIButton *)stackInfoButton
{
if(_stackInfoButton == nil)
{
self.stackInfoButton = [UIButton buttonWithType:UIButtonTypeInfoDark]; // theoretically your app should crash now
[self.stackInfoButton setShowsTouchWhenHighlighted:YES];
}
return _stackInfoButton;
}
Look at self.stackInfoButton = [UIButton buttonWithType:UIButtonTypeInfoDark];
It must be
_stackInfoButton = [UIButton buttonWithType:UIButtonTypeInfoDark];
[_stackInfoButton setShowsTouchWhenHighlighted:YES];
maybe it helps.
Normal selection of cell on uitableview is working fine. But when long press event is called for a cell it selects the previously selected cell again. For example if user selected 1st cell then long press the second cell ,, event for long press is called for the second cell but selection goes back again to first cell.
this is my codes:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"MyIdentifier"];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"MyIdentifier"];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
//add longPressGestureRecognizer to your cell
UILongPressGestureRecognizer *lpgr = [[UILongPressGestureRecognizer alloc]
initWithTarget:self action:#selector(handleLongPress:)];
//how long the press is for in seconds
lpgr.minimumPressDuration = 1.0; //seconds
[cell addGestureRecognizer:lpgr];
}
return cell;
}
-(void)handleLongPress:(UILongPressGestureRecognizer *)gestureRecognizer
{
CGPoint p = [gestureRecognizer locationInView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:p];
if (indexPath == nil) {
NSLog(#"long press on table view but not on a row");
}
else
{
if (gestureRecognizer.state == UIGestureRecognizerStateBegan)
{
NSLog(#"long press on table view at row %ld", (long)indexPath.row);
editViewController *editView = [self.storyboard instantiateViewControllerWithIdentifier:#"editView"]; //dont forget to set storyboard ID of you editViewController in storyboard
[self.navigationController pushViewController:editView animated:YES];
}
}
}
It seems that the issue you are having is rooted in the fact that marking a cell as selected and handling a long press gesture event on that cell are separate. My interpretation of your question is that you have a table view with single selection (not multiple) and that you want the cells to become 'selected' both via the normal tap to select action and also by recognizing a long press gesture. However -- while you want the cell to become selected with the long press gesture, you would like the longpress to result in a different action than selection via a normal tap (e.g. if the user taps a cell you want to launch view controller A but if the user long presses that cell you want to launch view controller B, and in both cases you want the table to regard the cell as 'selected) ...let me know if what you want is different than this, and I can update the answer.
This isn't a very common behavior for table views, but we can make it work by modifying your code a bit:
First define a property to keep track of whether a long press is happening:
#property (assign, nonatomic) BOOL longPressActive;
Then in your handleLongPress method, tell the table to select the row:
-(void)handleLongPress:(UILongPressGestureRecognizer *)gestureRecognizer
{
CGPoint p = [gestureRecognizer locationInView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:p];
if (indexPath == nil) {
NSLog(#"long press on table view but not on a row");
}
else
{
if (gestureRecognizer.state == UIGestureRecognizerStateBegan)
{
NSLog(#"long press on table view at row %ld", (long)indexPath.row);
self.longPressActive = YES;
[self.tableView selectRowAtIndexPath:indexPath
animated:NO
scrollPosition:UITableViewScrollPositionNone];
}else if (gestureRecognizer.state == UIGestureRecognizerStateEnded ||
gestureRecognizer.state == UIGestureRecognizerStateCancelled) {
self.longPressActive = NO;
}
}
Finally, in your table view delegate methods, define the behavior you expect after selection. Note that in the is example a long press on any cell will result in the same view controller displaying. In order to set that view controller up differently you can follow a process similar to my answer in your prior question or you can pass row specific data to the editViewController after you instantiate it.
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.longPressActive) { //Perform action desired when cell is long pressed
editViewController *editView = [self.storyboard instantiateViewControllerWithIdentifier:#"editView"];
[self.navigationController pushViewController:editView animated:YES];
}else { //Perform action desired when cell is selected normally
//Your code here
}
}
Hopefully that's helpful.
I have a split view controller: view & tableview. In this table view I have custom cells with text field. I don't know how many cells will I have, so it generate automatically. And now I'm trying scroll to textfield, when it becomeFirstResponder. I've tried something like this:
-(void) textFieldDidBeginEditing:(UITextField *)textField {
CGPoint focusOnTextField = CGPointMake(0, 300 + textField.frame.origin.y);
[scroller setContentOffset: focusOnTextField animated: YES];
}
300px - start position of my TableView. All seems alright, but textField.frame.origin.y always equal to 0 (like and bounds.origin.y btw).
I thought I can solve a problem if get position of cell, which textfield is active, and then replace textField.frame.origin.y on cell.frame.origin.y or something like this.
===================================================================
I forgot say that my tableviews scroll is disabled. I follow your advices and code examples and solve it like that:
- (UITableViewCell *)cellWithSubview:(UIView *)subview {
while (subview && ![subview isKindOfClass:[UITableViewCell self]])
subview = subview.superview;
return (UITableViewCell *)subview;
}
- (void)textFieldDidBeginEditing:(UITextField *)textField {
UITableViewCell *activeCell = [self cellWithSubview:textField];
float offsetValueY = 200 + activeCell.origin.y;
CGPoint focusOnTextField = CGPointMake(0, offsetValueY);
[scroller setContentOffset:focusOnTextField animated:YES];
}
And know what? It's working! :-) But it create a new problem. When I begin editing textfield, scroller at first jump on top, and only then going to it correct position. When I write [scroller setContentOffset:focusOnTextField animated:NO]; and this problem disappear, but there is no smooth move of scroller. And this is bad to me :-) So how we can solve it?
Here's how to scroll to a cell containing a text field ...
// find the cell containing a subview. this works independently of how cells
// have been constructed.
- (UITableViewCell *)cellWithSubview:(UIView *)subview {
while (subview && ![subview isKindOfClass:[UITableViewCell self]])
subview = subview.superview;
return (UITableViewCell *)subview;
}
Your idea is correct to trigger that action when editing begins. Just do it using the cell...
- (void)textFieldDidBeginEditing:(UITextField *)textField {
// find the cell containing this text field
UITableViewCell *cell = [self cellWithSubview:textField];
// now scroll using that cell's index path as the target
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
}
If you add the textfield in the UITableVieCell content view (added by default if you are using .xib) then you have to call something like textField.superview.superview and this will give you the parent cell. If you add the text field directly to the cell view then you have to textField.superview.
[tableView scrollToRowContainingComponent:textField atScrollPosition: UITableViewScrollPositionMiddle animated:YES];
after adding the following category to UITableView:
#implementation UITableView (MyCategory)
-(NSIndexPath*)indexPathOfCellComponent:(UIView*)component {
if([component isDescendantOfView:self] && component != self) {
CGPoint point = [component.superview convertPoint:component.center toView:self];
return [self indexPathForRowAtPoint:point];
}
else {
return nil;
}
}
-(void)scrollToRowContainingComponent:(UIView*)component atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated {
NSIndexPath *indexPath = [self indexPathOfCellComponent:component];
if(indexPath) {
[self scrollToRowAtIndexPath:indexPath atScrollPosition: scrollPosition animated:animated];
}
}
#end