I am creating a Chinese learning app, with a section for correctly pronouncing certain sounds. To organise these sounds into a table, I have created a UICollectionView, where each UICollectionViewCell contains a UITableView.
Each UICollectionViewCell acts as a column, and contains a UITableView.
Each UITableViewCell acts as a row in each column.
This results in the following appearance.
However, although I can easily detect taps on each UICollectionViewCell using collectionView:didSelectItemAtIndexPath: , I am unable to detect any touches at all on the UITableViewCells within.
Whether I convert the view in the Cell to be a button, or whether I as a UITapGestureRecognizer, or whether I use the function tableView:didSelectItemAtIndexPath, no touches at all are detected in the UITableView.
How can I allow my code to correctly detect these touches on the UITableViewCells within the UICollectionView?
Thanks!
Update 1:
Here is the #interfact of the Column .h file (UICollectionViewCell)
#interface PronounciationColumn : UICollectionViewCell <UITableViewDelegate, UITableViewDataSource>
In the .m of the UICollectionViewCell, the delegate and dataSource of the tableView is set to self.
Update 2:
Another user had a really neat idea of setting the delegate of the tableView to it's own collectionViewCell using the following code:
- (PronounciationColumn *)collectionView:(nonnull UICollectionView *)collectionView cellForItemAtIndexPath:(nonnull NSIndexPath *)indexPath {
PronounciationColumn *column;
NSString *columnIdentifier = [self getIdentifier:collectionView];
if (column == nil) {
column = (PronounciationColumn *)[collectionView dequeueReusableCellWithReuseIdentifier:columnIdentifier forIndexPath:indexPath];
column.arrayOfFinals = [[self getArrayOfPinyins:collectionView] objectAtIndex:indexPath.item];
}
column.tableOfFinals.delegate = column;
column.tableOfFinals.dataSource = column;
return column;
}
The tableView continues to be populated correctly, however, the touches are still not detected on the cells, as tableView:didSelectRowAtIndexPath: is still not called.
I'm using a button inside a tableView in which I get the indexPath.row when is pressed. But it only works fine when the cells can be displayed in the screen without scroll.
Once the tableView can be scrolleable and I scrolls throught the tableview, the indexPath.row returned is a wrong value, I noticed that initially setting 20 objects, for example Check is just printed 9 times no 20.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
lBtnWithAction = [[UIButton alloc] initWithFrame:CGRectMake(liLight1Xcord + 23, 10, liLight1Width + 5, liLight1Height + 25)];
lBtnWithAction.tag = ROW_BUTTON_ACTION;
lBtnWithAction.titleLabel.font = luiFontCheckmark;
lBtnWithAction.tintColor = [UIColor blackColor];
lBtnWithAction.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
[cell.contentView addSubview:lBtnWithAction];
}
else
{
lBtnWithAction = (UIButton *)[cell.contentView viewWithTag:ROW_BUTTON_ACTION];
}
//Set the tag
lBtnWithAction.tag = indexPath.row;
//Add the click event to the button inside a row
[lBtnWithAction addTarget:self action:#selector(rowButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
//This is printed just 9 times (the the number of cells that are initially displayed in the screen with no scroll), when scrolling the other ones are printed
NSLog(#"Check: %li", (long)indexPath.row);
return cell;
}
To do something with the clicked index:
-(void)rowButtonClicked:(UIButton*)sender
{
NSLog(#"Pressed: %li", (long)sender.tag);
}
Constants.h
#define ROW_BUTTON_ACTION 9
What is the correct way to get the indexPath.row inside rowButtonClicked or setting a tag when I have a lot of of cells in my tableView?
My solution to this kind of problem is not to use a tag in this way at all. It's a complete misuse of tags (in my opinion), and is likely to cause trouble down the road (as you've discovered), because cells are reused.
Typically, the problem being solved is this: A piece of interface in a cell is interacted with by the user (e.g. a button is tapped), and now we want to know what row that cell currently corresponds to so that we can respond with respect to the corresponding data model.
The way I solve this in my apps is, when the button is tapped or whatever and I receive a control event or delegate event from it, to walk up the view hierarchy from that piece of the interface (the button or whatever) until I come to the cell, and then call the table view's indexPath(for:), which takes a cell and returns the corresponding index path. The control event or delegate event always includes the interface object as a parameter, so it is easy to get from that to the cell and from there to the row.
Thus, for example:
UIView* v = // sender, the interface object
do {
v = v.superview;
} while (![v isKindOfClass: [UITableViewCell class]]);
UITableViewCell* cell = (UITableViewCell*)v;
NSIndexPath* ip = [self.tableView indexPathForCell:cell];
// and now we know the row (ip.row)
[NOTE A possible alternative would be to use a custom cell subclass in which you have a special property where you store the row in cellForRowAt. But this seems to me completely unnecessary, seeing as indexPath(for:) gives you exactly that same information! On the other hand, there is no indexPath(for:) for a header/footer, so in that case I do use a custom subclass that stores the section number, as in this example (see the implementation of viewForHeaderInSection).]
I agree with #matt that this is not a good use of tags, but disagree with him slightly about the solution. Instead of walking up the button's superviews until you find a cell, I prefer to get the button's origin, convert it to table view coordinates, and then ask the table view for the indexPath of the cell that contains those coordinates.
I wish Apple would add a function indexPathForView(_:) to UITableView. It's a common need, and easy to implement. To that end, here is a simple extension to UITableView that lets you ask a table view for the indexPath of any view that lies inside one of the tableView's cells.
Below is the key code for the extension, in both Objective-C and Swift. There is a working project on GitHub called TableViewExtension-Obj-C that illustrates the uses of the table view extension below.
EDIT
In Objective-C:
Header file UITableView_indexPathForView.h:
#import <UIKit/UIKit.h>
#interface UIView (indexPathForView)
- (NSIndexPath *) indexPathForView: (UIView *) view;
#end
UITableView_indexPathForView.m file:
#import "UITableView_indexPathForView.h"
#implementation UITableView (UITableView_indexPathForView)
- (NSIndexPath *) indexPathForView: (UIView *) view {
CGPoint origin = view.bounds.origin;
CGPoint viewOrigin = [self convertPoint: origin fromView: view];
return [self indexPathForRowAtPoint: viewOrigin];
}
And the IBAction on the button:
- (void) buttonTapped: (UIButton *) sender {
NSIndexPath *indexPath = [self.tableView indexPathForView: sender];
NSLog(#"Button tapped at indexpPath [%ld-%ld]",
(long)indexPath.section,
(long)indexPath.row);
}
In Swift:
import UIKit
public extension UITableView {
func indexPathForView(_ view: UIView) -> IndexPath? {
let origin = view.bounds.origin
let viewOrigin = self.convert(origin, from: view)
let indexPath = self.indexPathForRow(at: viewOrigin)
return indexPath
}
}
I added this as a file "UITableView+indexPathForView" to a test project to make sure I got everything correct. Then in the IBAction for a button that is inside a cell:
func buttonTapped(_ button: UIButton) {
let indexPath = self.tableView.indexPathForView(button)
print("Button tapped at indexPath \(indexPath)")
}
I made the extension work on any UIView, not just buttons, so that it's more general-purpose.
The nice thing about this extension is that you can drop it into any project and it adds the new indexPathForView(_:) function to all your table views without having do change your other code at all.
You are running into the issue of cell-reuse.
When you create a button for the view you set a tag to it, but then you override this tag to set the row number to it.
When the cell get's reused, because the row number is longer ROW_BUTTON_ACTION, you don't reset the tag to the correct row number and things go wrong.
Using a tag to get information out of a view is almost always a bad idea and is quite brittle, as you can see here.
As Matt has already said, walking the hierarchy is a better idea.
Also, your method doesn't need to be written in this way. If you create your own custom cell, then the code you use to create and add buttons and tags isn't needed, you can do it in a xib, a storyboard, or even in code in the class. Furthermore, if you use the dequeue method that takes the index path, you will always get either a recycled cell, or a newly created cell, so there is no need to check that the cell returned is not nil.
I have a UICollectionView with 1 row 1 column. I want to get the first item (indexPath basically) when I am scrolling it horizontally.
For example, I have got 100 items displayed horizontally in my UICollectionView and when I scroll from right to left, whichever item is the first one visible, I need its indexPath.
Which means the indexPath will be constantly changing while its scrolling.
How to achieve this? Please Guide. Thanks!
collectionView.indexPathsForVisibleItems() returns an array of NSIndexPath for visible cells. You can get the left most one by sorting them by NSIndexPath.item.
Edti:
To be notified while scrolling, implement the UIScrollViewDelegate method scrollViewDidScroll(_: UIScrollView). Remember that UICollectionView is a subclass of UIScrollView.
I think you need implement UIScrollView delegate method and get visible cell inside
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
UICollectionViewCell *firstCell = [[collectionView visibleCells] firstObject];
NSIndexPath *firstIndexPath = [collectionView indexPathForCell: firstCell];
NSLog(#"%#", firstIndexPath);
}
Hope this help
I have a Custom UITableView Cell as xib.
I have taken an Scroll View inside it.
I Know how to set the delegate but I have confusion.Their is two of doing this.
I don't knw which is best and How to decide which way I have to choose.
1 Way : To set Delegate To Files Owner
2 Way : setting Delegate to UITableViewCell
In a tableviewcell the delegate of scrollview will always be set to UITableViewCell
That means your second step will work .
Let me know if you find any difficulty.
The choice depends on what the delegate function for the inner scroll view must do. It is simpler to point the delegate to the custom cell and handle the inner scrolling events there.
But if handling that inner scrolling requires a lot of data and logic from the view controller, then point the delegate to the vc. To make that work, we'll need to know that the scroll event was from an inner scroll view, not the table, and we probably need the row where the scrolling happened, so:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// if the table view's delegate points here (likely), then to distinguish...
if (scrollView != self.tableView) {
// one of the scroll views in the table, but which one?
NSIndexPath = [self indexPathContainingView:scrollView];
// here we know that horizontal scrolling happened on indexPath.row
}
}
// return the indexPath of the tableView cell containing a view
- (NSIndexPath *)indexPathContainingView:(UIView *)view {
while(view && ![view isKindOfClass:[UITableViewCell self]])
view = view.superview;
return [self.tableView indexPathForCell:(UITableViewCell *)view];
}
I would like to know if there is a way to detect if the current UITableViewCell is the last one in view then when the user pressed enter you will scroll the UITableView using this code
int cellHeight = 44;
[finishingTableView setContentOffset:CGPointMake(finishingTableView.contentOffset.x,finishingTableView.contentOffset.y + cellHeight) animated:YES];
I have a UITableView with custom UITableViewCells, each UItableViewCell has a UITextfield and I am programmatically selecting the UITableViewCell then making the cells textfield become first responder. I want to allow the user to hit enter all the way to the bottom of the UITableView but only start scrolling when the next UITableViewCell to select is out of view.
Use the indexPathsForVisibleRows method of UITableView.
NSArray *visibleRows = [self.tableView indexPathsForVisibleRows];
NSIndexPath *lastRow = [visibleRows lastObject];
Then compare that to the indexPath you have for the cell you are dealing with.