Accessing index of UICollectionViewCell from a ScrollView inside the cell - ios

I have a collection view, and inside the collection view cell, I have a scrollView in which I want to display some pictures.
I placed a ScrollView inside my collectionviewcell in the storyboard and initialised the ScrollView inside my -collectionView:cellForItemAtIndexPath method as follows:
UIScrollView *scrollView = [cell viewWithTag:20];
scrollView.delegate = self;
[scrollView setContentSize:CGSizeMake((cell.frame.size.width*images.count), scrollView.frame.size.height)];
scrollView.userInteractionEnabled = NO;
[cell addGestureRecognizer:imagesScrollView.panGestureRecognizer];
Only the first image from the images array should be loaded when the view is first loaded and the rest of the images need to be loaded dynamically when the user scrolls the scrollview, in the -scrollViewDidScroll method. However, these images depend on the index of the collectionviewcell that my scrollView is lying in.
How can I access the index of the collectionviewcell from the -scrollViewDidScroll method?
Also, does this seem like a viable method to achieve what I'm trying to do, or will I need to subclass UICollectionViewCell?

Subclassing can be one of the solutions but you can get the indexPath of a UICollectionViewCell from its subviews too. The following code would hopefully serve your needs. Use it in your scrollViewDidScroll: method.
UIView *superview = scrollView;
while (![superview isKindOfClass:[UICollectionViewCell class]]) {
superview = superview.superview;
}
UICollectionViewCell *cell = (UICollectionViewCell *)superview;
NSIndexPath *indexPath = [self.collectionView indexPathForCell:cell];
NSLog(#"IndexPath: %#", indexPath);
Make sure you are not using a scrollView elsewhere in this viewController.

Related

Reuse of UICollectionViewCells during scrolling

I'm having an issue,
I have a simple UICollectionView with a static 200 cells that load images from Flickr.
my CellForItemAtIndexPath looks like this:
- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [cv dequeueReusableCellWithReuseIdentifier:#"FlickrCell" forIndexPath:indexPath];
cell.backgroundColor = [self generateRandomUIColor];
if(![[cell.subviews objectAtIndex:0] isKindOfClass:[PFImageView class]])
{
NSURL *staticPhotoURL = [self.context photoSourceURLFromDictionary:[self.photos objectAtIndex:indexPath.row] size:OFFlickrSmallSize];
PFImageView *imageView = [[PFImageView alloc] initWithFrame:CGRectMake(0, 0, cell.frame.size.height, cell.frame.size.width) andImageURL:staticPhotoURL andOwningCell:cell];
[cell addSubview:imageView];
}
return cell;
}
PFImageView is a subclass of UIImageView that loads a Flickr photo URL on a background thread and then updates it's own image on the main thread - this works fine.
The logic is really simple - I create a cell if there isn't one dequeueable.
If the cell (which I'm expecting to be dequeued and already have a PFImageView) doesn't have a PFImageView, I alloc and init an imageView for the cell and add it as a subview of the cell.
Thus I expect if the cell has been dequeued it should already have a PFImageView as a subview and as we should not get into the if statement to create a new imageView and kick off a new photo download request
Instead what I see is that the cells at the top and bottom of the UICollectionView that 'go off screen' momentarily - when they come back on screen they are not being reused and seemingly a new cell is created and the picture refreshed.
1) How can I achieve a static image once the cell has been created (i.e. not refreshing when the cell goes slightly off screen.
2) Why are the cells not being reused?
Many thanks for your time.
John
UICollectionView will reuse cells for maximum efficiency. It does not guarantee any particular reuse or population strategies. Anecdotally, it seems to place and remove cells based on integer power of two regions — e.g. on a non-retina iPad it might divide your scroll area up into regions of 1024x1024 and then populate and depopulate each of those regions as they transition into and out of the visible area. However you should not predicate any expectations on its exact behaviour.
In addition, your use of collection view cells is incorrect. See the documentation. A cell explicitly has at least two subviews — backgroundView and contentView. So if you add a subview it will be at index 2 at the absolute least and, in reality, the index will be undefined. In any case you should add subviews to contentView, not to the cell itself.
The most normal way of doing what you're doing would be to create a custom UICollectionView subclass that inherently has a PFImageView within it.
I see several potential issues:
You are looking specifically at index 0 of the cell for the child class that you are adding. The UICollectionViewCell may have other views as children, so you can't just assume that the only (or first) child is the one you added.
I don't see that you are calling registerClass:forCellWithReuseIdentifier: or registerNib:forCellWithReuseIdentifier:, one of which is required for proper use of dequeue (https://developer.apple.com/library/ios/documentation/uikit/reference/UICollectionViewCell_class/Reference/Reference.html).
You are only setting the URL of the PFImageView in the case that you have to construct the PFImageView. The idea with dequeuing reusable views is that you will only construct a small subset of the views needed, and the UITableView will recycle them as they move offscreen. You need to reset the value for the indexPath that is being requested, even when you don't construct the new content.
If your case is as simple as you describe, you can probably get away with adding your PFImageView to the contentView property of your dequeued UICollectionView.
In your controller:
// solve problem 2
[self.collectionView registerClass:[UICollectionViewCell class] forReuseIdentifer:#"FlickrCell"];
In collectionView:cellForItemAtIndexPath
UICollectionViewCell *cell = [cv dequeueReusableCellWithReuseIdentifier:#"FlickrCell" forIndexPath:indexPath];
cell.backgroundColor = [self generateRandomUIColor];
// solve problem 1 by looking in the contentView for your subview (and looping instead of assuming at 0)
PFImageView *pfImageView = nil;
for (UIView *subview in cell.contentView.subviews)
{
if ([subview isKindOfClass:[PFImageView class]])
{
pfImageView = (PFImageView *)subview;
break;
}
}
NSURL *staticPhotoURL = [self.context photoSourceURLFromDictionary:[self.photos objectAtIndex:indexPath.row] size:OFFlickrSmallSize];
if (pfImageView == nil)
{
// No PFImageView, create one
// note the use of contentView!
pfImageView = [[PFImageView alloc] initWithFrame:CGRectMake(0, 0, cell.contentView.frame.size.height, cell.frame.size.width) andImageURL:staticPhotoURL andOwningCell:cell.contentView];
[cell.contentView addSubview:pfImageView];
}
else
{
// Already have recycled view.
// need to reset the url for the pfImageView. (Problem 3)
// not sure what PFImageView looks like so this is an e.g. I'd probably remove the
// URL loading from the ctr above and instead have a function that loads the
// image. Then, you could do this outside of the if, regardless of whether you had
// to alloc the child view or not.
[pfImageView loadImageWithUrl:staticPhotoURL];
// if you really only have 200 static images, you might consider caching all of them
}
return cell;
For less simple cases (e.g. where I want to visually lay out the cell, or where I have multiple children in the content), I typically customize my UICollectionViewCell's using Interface Builder.
Create a subclass of UICollectionViewCell in the project (In your case, call it PFImageCell).
Add an IBOutlet property to that subclass for the view I want to change in initialization (In your case, a UIImageView).
#property (nonatomic, assign) IBOutlet UIImageView *image;
In Interface Builder, create a prototype cell for the UITableView.
In the properties sheet for that prototype cell, identify the UICollectionViewCell subclass as the class.
Give the prototype cell an identifier (the reuse identifier) in the property sheet.
Add the view child in interface builder to the prototype cell (here, a UIImageView).
Use IB to map the IBOutlet property to the added UIImageView
Then, on dequeue in cellForRowAtIndexPath, cast the dequeued result to the subclass (PFImageCell) and set the value of the IBOutlet property instance. Here, you'd load the proper image for your UIImageView.
I am not sure if the cell is being re-used or not. It may be being reused but the subview may not be there. My suggestion would be to create a PFImageViewCollectionViewCell Class (sub class of UICollectionViewCell) and register it as the CollectionView Cell and try. That's how I do and would do if I need a subview inside a cell.
Try adding a tag on this particular UIImageView
- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath {
static int photoViewTag = 54353532;
UICollectionViewCell *cell = [cv dequeueReusableCellWithReuseIdentifier:#"FlickrCell" forIndexPath:indexPath];
cell.backgroundColor = [self generateRandomUIColor];
PFImageView *photoView = [cell.contentView viewWithTag:photoViewTag];
// Create a view
//
if (!photoView) {
photoView = [[PFImageView alloc] initWithFrame:CGRectMake(0, 0, cell.frame.size.height, cell.frame.size.width) andImageURL:staticPhotoURL andOwningCell:cell];
imageView.tag = photoViewTag;
[cell.contentView addSubview:imageView];
}
// Update the current view
//
else {
NSURL *staticPhotoURL = [self.context photoSourceURLFromDictionary:[self.photos objectAtIndex:indexPath.row] size:OFFlickrSmallSize];
photoView.imageURL = staticPhotoURL;
}
return cell;
}
I would really recommend to create your own UICollectionViewCell subclass though.
EDIT: Also, note that I used the contentView property instead of adding it directly to the cell.

How to get the cell reference from a gesture of another item in the cell

I have a UITableView which has some custom cells. Within the custom cells I have a main UIImageView. When the cell is created I add a Tap Gesture Recogniser to the image view.
When the image is tapped I run the following:
- (void) handleImageTap:(UIGestureRecognizer *)gestureRecognizer {
NSLog(#"Image tapped");
UIImageView *imageView = (UIImageView *)gestureRecognizer.view;
// send the image instead of self when firing the segue
[self performSegueWithIdentifier:#"remindMeTurnInfo" sender:imageView];
}
I then pass the image into a new view controller in prepareForSegue method:
if ([segue.identifier isEqualToString:#"remindMeTurnInfo"]) {
UIImageView *imgView = (UIImageView *)sender;
MESPlayedTurnReminderViewController *VC = segue.destinationViewController;
VC.turnImage = imgView.image;
}
Question
1. I need a reference to the cell that the UIImageView is within, that was tapped. So the user taps one of the cells images, and I need to know which cell (indexPath) that image was tapped from. 2. I am finding that sometimes when the image is tapped the didSelectRowAtIndexPath is being called. This is incorrect and it should not be called, only the relevant handleTap method from the Gesture Rec. should be called. How can I ensure that the didSelectRowAtIndexPath is not called, as I need to run some other code when the cell is actually (correctly) selected.
There are two ways this is commonly done. Either add a tag to your image view in cellForRowAtIndexPath that's equal to indexPath.row, or search up through the hierarchy of views from the image view until you find one that's a UITableViewCell (or subclass). That can be done like this:
- (void) handleImageTap:(UIGestureRecognizer *)gestureRecognizer {
UIImageView *imageView = (UIImageView *)gestureRecognizer.view;
UIView *superview = imageView.superview;
while (![superview isKindOfClass:[UITableViewCell class]]) {
superview = superview.superview;
}
NSLog(#"indexPath.row is: %d", [self.tableView indexPathForCell:(UITableViewCell *)superview].row);
}
The reason to search rather than using something like imageView.superview.superview (which would work in iOS 6), is that the hierarchy can change in different versions of iOS, and in fact, it did change between iOS 6 and iOS 7.
As for your second question, it probably happens because you're accidentally tapping the cell rather than the image view. Other than making the image view larger so it's easier to tap, I don't see a fix for that.
1.- To get the cell you can check the superview of the UIImageView tapped
- (void) handleImageTap:(UIGestureRecognizer *)gestureRecognizer
{
UIImageView *imageView = (UIImageView *)gestureRecognizer.view;
UITableViewCell *cell = (UITableViewCell *)imageView.superview;
}
2.- To disable the cell selection on a UITableView
[self.yourTableView setAllowsSelection:NO];

How to pass gestures from UITextView to UICollectionViewCell

I have a horizontal scrolling UICollectionView with UICollectionViewCells that contain a UITextView. Is there any way to pass gestures on the textview to the cells, so that didSelectItemAtIndexPath gets called?.
I tried it with subclassing UITextView and passing touchesbegin/end to the cell, but that didn't worked.
You can make the view non-interactive, which will cause touches to get passed through:
textView.userInteractionEnabled = NO;
If you need it to be interactive, you can try this:
textView.editable = NO;
UITapGestureRecognizer* tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapped)];
[textView addGestureRecognizer:tap];
... and then add this function to your UICollectionViewCell subclass:
-(void) tapped {
UICollectionView *collectionView = (UICollectionView*)self.superview;
NSIndexPath *indexPath = [collectionView indexPathForCell:self];
[collectionView.delegate collectionView:collectionView didSelectItemAtIndexPath:indexPath];
}
I haven't tested it though...
Well, if your cell is the superview of the text view, you could implement something like this in the UITextViewDelegate method textViewDidBeginEditing:.
- (void)textViewDidBeginEditing:(UITextView *)textView {
NSIndexPath *indexPath = [self.collectionView indexPathForCell:(UICollectionViewCell *)textView.superview];
[self.collectionView selectItemAtIndexPath:indexPath animated:YES scrollPosition:UICollectionViewScrollPositionTop];
}
This doesn't seem to work in iOS6.x: the all view in a UICollectionViewCell seem to be embedded in a UIView that is the first child of the cell. In order to get the actual cell that is the UITextView is in you will need to dereference a second time. In other words the order is (from bottom to top):
UITextView->enclosingUIView->UICollectionViewCell

How do I reach a sender's parent view?

I have a button inside a custom cell. When tapped the button presents a view via a modal segue.
So, here's my question, in the prepareForSegue:sender: I want to pull the indexPath of the cell which contains that button(sender) that I pushed. How do I get that cell's indexPath?
I thought I could do something like:
UITableViewCell * cell = sender.parent
Obviously it doesn't work that way.
Please help me out.
UPDATE Thanks to #rmaddy I've tried [[sender superview] superview] and got to the cell in which the button was held and to it's indexPath. Thanks for your answers guys!
Assuming sender is a UIVIew of some sort (such as a UIButton), then:
UIView *parentView = [(UIView *)sender superview];
If you are positive that the button's superview is the table cell then you can do:
UITableViewCell *cell = (UITableViewCell *)[(UIView *)sender superview];
Once you have the cell, use the table view's indexPathForCell: method.
Keep in mind that if you actually added the button to the cell's contentView or some other subview of the cell, then getting the cell from the button is a bit trickier.
You can access a view's superview with the superview property so
sender.superview
Just add in cellForRow... method
cellButton.tag = indexPath.row;
and get it back when this button is pressed or you have the reference of the button
try this
UIButton *btn = (UIButton*)sender;
UITableViewCell * cell = (UITableViewCell*)[btn superview];
NSIndexPath *indx = [myTable indexPathForCell:cell];
There is a hidden element added to the cell: UITableViewCellScrollView. UITableViewCellScrollView is added between the UITableViewCell and the content view. The first sender superview will return an UIView object corresponding the content view. The second superview will return an UITableViewCellScrollView and the third superview will return UITableViewCell. Then use the indexPathForCell method to get the cell's index.
UITableViewCell *cellView = (UITableViewCell *)[[[sender superview] superview] superview];
NSIndexPath *indexPath = [self.tableView indexPathForCell:cellView];

Identifying the table cell, when tapping on a Tap Recogniser

I have a tableview in my View. The cells are created using custom Cells. I need to display a large string in the table view cells So I had added the text Label in a Scrollview. Also I need to execute some code when the user taps on table view cell. Please see the below code:
[cell.textLabelLine2 setFrame:CGRectMake(cell.textLabelLine2.frame.origin.x, cell.textLabelLine2.frame.origin.y, 500, cell.textLabelLine2.frame.size.height)];
cell.scrollView.contentSize = CGSizeMake(cell.textLabelLine2.text.length*10 , 10);
cell.scrollView.pagingEnabled = NO;
The problem is when the user touches above the Scroll View, the Tableview did select method will not be called. The solution I found for this problem is to add a gesture recogniser to the scroll view. But in this solution, we have no ways to check which cell(or which gesture recogniser) was selected. Could anyone help me to find a solution for this problem?
You can get to know the cell by the following code
if(gestureRecognizer.state == UIGestureRecognizerStateBegan) {
CGPoint p = [gestureRecognizer locationInView:[self tableView]];
NSIndexPath *indexPath = [[self tableView] indexPathForRowAtPoint:p];
if(indexPath != nil) {
UITableViewCell *cell = [[self tableView] cellForRowAtIndexPath:indexPath];
...
}
}
It's generally a bad idea putting scroll views inside scroll views. UITableView is also just a UIScrollView. That only kind of works if they are scrolling on different axis, i.e. the outer scroll view scrolling vertically and the inner scrolling horizontally.
For your specific scenario you would have to trigger the selection yourself. Once you have a reference to the cell you can ask the table view for the indexPath of it. Then you would call the delegate method for didSelectRow... yourself.
In the solution with the scrollview you are not able to scroll in the scrollview because the gestureRecognizer 'gets' the touch. Therefor I would not use the scrollview at all.
Make the label resize to its content like:
CGSize customTextLabelSize = [cell.customTextLabel.text sizeWithFont:cell.customTextLabel.font constrainedToSize:CGSizeMake(cell.customTextLabel.frame.size.width, 999999)];
cell.customTextLabel.frame = CGRectMake(cell.customTextLabel.frame.origin.x, cell.customTextLabel.frame.origin.y, cell.customTextLabel.frame.size.width, customTextLabelSize.height);
You also need to implement this in the heightForRowAtIndexPath
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
CGSize cellSize = [bigTextString sizeWithFont:customTextLabel.font constrainedToSize:CGSizeMake(generalCellWidth, 999999)];
return cellSize.height;
}
This way you can just use the didSelectRowAtIndex method.
If you really want to use the scrollview, add a button to your cell in the cellForRowAtIndexPath: method. Make the button just as big as the cell and add a button tag like this:
UIButton *cellButton = [UIButton buttonWithType:UIButtonTypeCustom];
cellButton.frame = CGRectMake(0, 0, cell.frame.size.width, cell.frame.size.height);
cellButton.tag = indexPath.row;
[cellButton addTarget:self action:#selector(cellButtonAction:) forControlEvents:UIControlEventTouchUpInside];
[cell.contentView addSubview:cellButton];
Then add:
-(void)cellButtonAction:(UIButton*)sender
{
//do something with sender.tag
}

Resources