How to track movement of a cell in UICollectionView after screen rotation - ios

I have an UICollectionView. When I touch one of the cells I present a popover view from its location with the arrow pointing to the cell with some extra information about the cell.
When I rotate the device, the UICollectionView automatically repositions its cells.
What I'd like to do, is to reposition my popover view automatically so it points to the same cell as before (which is now at a different location in the UICollectionView)
What I'm having trouble is tracking (finding out) what is my cell's new location, so I can manually represent the popover view from the cell's new location.
I tried attaching and storing an "idString" for the cell for comparison, but this for some reason returns the cell's old frame from before the screen rotation
NSArray* visibleCells = [UIAppDelegate.ocollectionView visibleCells];
for (UICollectionViewCell *cell in visibleCells) {
if ([cell.idString isEqualToString:self.idString] ) {
NSLog (#"we have a match!!! %#", cell);
CGRect rectInCollectionView = cell.frame;
rect = [UIAppDelegate.collectionView convertRect:rectInCollectionView toView:[UIAppDelegate.collectionView superview]];
}
}
any ideas appreciated. thank you.

Where is this code being placed? I guess it should be placed on a delegate method like this kind:
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation;
So the UICollectionView has redrawn its new cell's position.

Related

UICollectionView scrollToItemAtIndexPath: and get fresh cell position

I would like to scroll collection view to some offscreen cell without animation and get position of that cell after scrolling. The problem is that collection view scrolls correctly (visually) but cell frame remains offscreen. Here is my code:
NSIndexPath *path = [fetchedResultsController indexPathForObject:objectInCollection];
[collectionView scrollToItemAtIndexPath:path atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:NO];
[collectionView layoutSubviews]; // or 'layoutIfNeeded', doesn't matter
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:path];
cellFrame = [cell.superview convertRect:cell.frame toView:nil]; // still not refreshed
I can propose that collection view applies scrolling not immediately, so I would like to find approach to apply the scrolling.
--- UPDATE ---
The collection view is in previous view controller (so I scroll it from code and then pop visible view controller).
Cell frame remains offscreen even in viewDidAppear: method of previous view controller. To be exact, [cell.superview convertRect:cell.frame toView:nil]; returns frame without contentOffset of collection view. So CGRectOffset([cell.superview convertRect:cell.frame toView:nil], 0, -collectionView.contentOffset.y) returns correct cell frame on screen.
As i can understand from your problem statement, you want to set the some cell which is out of the view to visible state.
The cells are placed at the same position when the UICollectionView calculates
(void)prepareLayout
(void)layoutAttributes.. methods.
The frames of the collection cells are fixed till the invalidateLayout or reloaddata is called. Just you have to work around with the contentOffset of the collectionview.
Just get the frame of cell and set the contentOffset so the your frame of cell is visible on the screen. Hope it helps.
The cell frame should be correct and I don't think you need to use convertRect at all.
cellFrame = cell.frame;
However, you probably have to do this after the collection view loads. Call [collectionView reloadData] first.
There is also a layoutAttributesForItemAtIndexPath: method that has the attributes for that cell. You can call this on the indexPath and get the attributes that way.
Try changing scroll position to UICollectionViewScrollPositionBottom, this works for me -
[self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionBottom animated:NO];

Get the Center Point of a Cell Embedded in Scroll View

I have a UICollectionView embedded in a scroll view:
Each white square is a collection view cell. The user can scroll horizontally to reveal additional cells.
When the user clicks on a cell, I have created an animation which causes that cell to expand from its own center outward as a transition is made to a new view controller.
Here is the code:
//cell is selected:
//grab snapshot of cell
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
UIImage *cellImage = [self imageOfCollectionCell:cell];
//hold the snapshot in this dict
NSMutableDictionary *cellImageDict = [[NSMutableDictionary alloc]init];
[cellImageDict setObject:cellImage forKey:SELECTED_INBOX_EMAIL_IMAGE];
[ViewControllerSnapShotDictionary sharedInstance].vcdict = nil;
[ViewControllerSnapShotDictionary sharedInstance].vcdict = cellImageDict;
//get the center of the cell (HERE IS WHERE THE PROBLEM IS)
CGPoint cellCenter = CGPointMake(cell.center.x, cell.center.y);
//do the animation using the snapshot and cell center
[self startPinchingOutCellFromCenterPoint:cellCenter forTransitionTo:emailController withSnapShotImage:SELECTED_INBOX_EMAIL_IMAGE];
The code works fine, EXCEPT if the collection view has been scrolled. The animation requires that I know where the center of the cell is at the moment it is on screen, being touched and relative to the coordinates of the view I am looking at.
For example, if the collection view has not been scrolled, and I select the center cell of the above image, the center might return as:
cell.center.x = 490
cell.center.y = 374
However, if I do a scroll to the right, and then select the new center cell, I might get something like:
cell.center.x = 1770
cell.center.y = 374
My question is, is there either a better way to accomplish what I am trying to do, OR is there a way to get a handle on the center of the cell as it lies in its current position in self.view?
I think this is because the center coordinates are in the collectionView's coordinate system (which scrolls). You need to convert it to a coordinate system that doesn't scroll.
Try something like this:
CGPoint realCenter = [collectionView convertPoint:cell.center
toView:collectionView.superview];
What it does, is basically converts the center from the collectionView's coordinate system to it's parent which should be fixed (not scrolling).
You can even obtain the coordinate on the screen(window) by passing nil as parameter:
CGPoint realCenter = [collectionView convertPoint:cell.center
toView:nil];
It's up to you to decide to which coordinate you want to convert.

Redirecting tap behavior to another layer in a UICollectionViewCell

I am trying to figure out how to change the view in a UICollectionViewCell that triggers the UICollectionView's didSelectCellAtIndexPath. I want to have a top layer that can be tapped to activate, or panned to reveal a lower layer of buttons.
I can add a view on top, and achieve the pan to reveal gesture, but I have to tap the underlying view to trigger didSelect.
Also, I am not using xib's or storyboards, this is all in code.
Any Ideas?
Thanks to #peko, I figured it out.
- (void)selectCell {
UICollectionView *collectionView = (UICollectionView *)self.superview;
NSIndexPath *indexPath = [collectionView indexPathForCell:self];
[collectionView.delegate collectionView:collectionView didSelectItemAtIndexPath:indexPath];
}
This allows me to simulate selection of the cell from within the view controller.

Getting the screen location of a cell from a UICollectionView

This isn't so much a question as an explanation of how to solve this problem.
The first thing to realize is that the UICollectionView does inherit from a UIScrollView - so doing a standard lookup with a scroll view's content is the best solution.
Here's the problem I was addressing:
I had a UICollectionView that had differing items in each cell - along with differing types of cells. I need the selection of a cell to cause an effect of the image in the cell to appear to expand and take over the whole screen. How I did the expansion is for another post.
The challenge was getting the cell's position on the screen so that the animating section would have a reference point from where to start.
So, to facilitate getting this information - consider the following code:
First note:
UICollectionView *picturesCollectionView;
DrawingCell cell; // -> instanceof UICollectionViewCell with custom items.
// first, get the list of cells that are visible on the screen - you must do this every time
// since the items can change... This is a CRITICAL fact. You do not go through the
// entire list of cells - only those the collectionView indicates are visible. Note
// there are some things to watch out for - the visibles array does not match the indexPath.item
// number - they are independent. The latter is the item number overall the cells, while
// the visibles array may have only 2 entries - so there is NOT a 1-to-1 mapping - keep
// that in mind.
NSArray *visibles = [self.picturesCollectionView visibleCells];
// now, cycle through the times and find the one that matches some criteria. In my
// case, check that the cell for the indexPath passed matches the cell's imageView...
// The indexPath was passed in for the method call - note that the indexPath will point
// to the number in your datasource for the particular item - this is crucial.
for (int i=0; i<visibles.count; i++) {
DrawingCell *cell = (DrawingCell *)visibles[i];
if (cell.imageView.image == (UIImage *)images[indexPath.item]) {
// at this point, we've found the correct cell - now do the translation to determine
// what is it's location on the current screen... You do this by getting the contentOffset
// from the collectionView subtracted from the cell's origin - and adding in (in my case)
// the frame offset for the position of the item I wish to animate (in my case the
// imageView contained within my custom collection cell...
CGFloat relativeX = cell.frame.origin.x - self.picturesCollectionView.contentOffset.x + cell.imageView.frame.origin.x;
CGFloat relativeY = cell.frame.origin.y - self.picturesCollectionView.contentOffset.y + cell.imageView.frame.origin.y;
// I now have the exact screen coordinates of the imageView - so since I need this
// to perform animations, I save it off in a CGRect - in my case, I set the size
// exactly to the size of the imageView - so say you were doing a Flicker display
// where you wanted to grow a selected image, you get the coordinates of the image
// in the cell and the size from the displayed image...
UIImageView *image = cell.imageView;
// selectedCell is a CGRect that's global for the sake of this code...
selectedCell = cell.frame;
selectedCell.origin.x = relativeX;
selectedCell.origin.y = relativeY;
selectedCell.size.width = cell.imageView.frame.size.width;
selectedCell.size.height = cell.imageView.frame.size.height;
}
}
// done. I have my coordinates and the size of the imageView I wish to animate and grow...
Hopefully, this helps other folks that are trying to figure out how to say overlay something on the cell in an exact position, etc...
-(void)collectionView:(UICollectionView *)cv didSelectItemAtIndexPath:(NSIndexPath *)indexPath;
{
UICollectionViewLayoutAttributes *attributes = [cv layoutAttributesForItemAtIndexPath:indexPath];
CGRect cellRect = attributes.frame;
CGRect cellFrameInSuperview = [cv convertRect:cellRect toView:[cv superview]];
NSLog(#"%f",cellFrameInSuperview.origin.x);
}
It work for me.You can try yourself
Well the first part of your question is pretty much clear, the second one?? anyway
if what you want to get is the frame of the select cell in your collection you can use this :
UICollectionViewLayoutAttributes *attributes = [self.collectionView layoutAttributesForItemAtIndexPath:indexPath];
CGRect cellRect = attributes.frame;
More info here
#Alivin solution using layoutAttributesForItemAtIndexPath works but only for the presented/current scroll view that the user sees.
Meaning, if you select the first presented visible cells you will get the actual frame, but if you scroll, the frame will have a deviation and you won't get the coordinates you need.
This is why you need to use convertPoint:toView :
let realCenter = collectionView.convertPoint(cell.center, toView: collectionView.superview)
Basically this method takes a point (cell.center) in one view and convert that point to another view (collectionView.superview) coordinate system which is exactly what we need.
Thus, realCenter will always contain the coordinates to the actual selected cell.
I've done this before as well. it took a while but it is possible.
You need to use
[currentImageView.superview convertRect:currentImageView.frame toView:translateView]
Where currentImageView is the image that the user taps. It's superview will be the cell.
You want to convert the rect of your image to where it actually is on a different view. That view is called "translateView" here.
So what is translateView? In most cases it is just self.view.
This will give you a new frame for your imageview that will meet where your image is on your table. Once you have that you can expand the image to take up the entire screen.
Here is a gist of the code I use to tap an image then expand the image and display a new controller that allows panning of the image.
https://gist.github.com/farhanpatel/4964372
I needed to know the exact location of the cell's center that a user tapped relative to the UIWindow. In my situation the collectionView was a child of a view that took up 2/3 of the screen and its superview was a child of another view. Long story short using the collectionView.superView wasn't suffice and I needed the window. I used Ohadman's answer above and this answer from TomerBu to get the tapped location of the screen/window's coordinate system.
Assuming your app has 1 window that it isn't connected across multiple screens, I used both of these and the same exact location printed out.
It's important to know that this is going to give you the exact middle of the cell (relative to the window). Even if you touch the top, left, bottom or right side of the cell it's going to return the coordinate of the center of the cell itself and not the exact location that you tapped.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let cell = collectionView.cellForItem(at: indexPath) as? YourCell else { return }
guard let layoutAttributes = collectionView.layoutAttributesForItem(at: indexPath) else { return }
guard let window = UIApplication.shared.keyWindow else { return }
let touchedLocationInWindow = collectionView.convert(cell.center, to: window)
print("OhadM's version: \(touchedLocationInWindow)")
let cPoint = layoutAttributes.center
let tappedLocationInWindow = collectionView.convert(cPoint, to: window)
print("TomerBu's version: \(tappedLocationInWindow)")
}
An alternative is to use the events to provide most if not all the answers to your questions.
I presume that a touch event will initiate all of this, so lets implement something meaningful;
//First, trap the event in the collectionView controller with;
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
// lets ensure it's actually visible (yes we got here from a touch event so it must be... just more info)
if ([self.collectionView.indexPathsForVisibleItems containsObject:indexPath]) {
// get a ref to the UICollectionViewCell at indexPath
UICollectionViewCell *cell =(UICollectionViewCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
//finally get the rect for the cell
CGRect cellRect = cell.frame
// do your processing here... a ref to the cell will allow you to drill down to the image without the headache!!
}
}
oh ... before you rush off for happy hour, lets not forget to read up on;
<UICollectionViewDelegate> (hint - it's needed)
SWIFTYFIED (v5) SHORT ANSWER
let attributes = collectionView.layoutAttributesForItem(at: indexPath)
let cellRect = attributes?.frame
let cellFrameInSuperview = collectionView.convert(cellRect ?? CGRect.zero, to: collectionView.superview)
All you need is the index path of the cell.

iPad: Keeping a selected table view cell on screen

On the iPad I show a UIPopover when the user selects cells in a UITableView. The cell stays selected until the popover is dismissed.
When the user rotates the device from portrait to landscape orientation and the selected cell was on the lower part of the screen, it will disappear after the rotation and the popover ends up pointing at another (indifferent) cell.
How can I make sure that the selected cell in a UITableView stays on screen when rotating from portrait to landscape orientation?
Update: Combining Caleb's and kviksilver's codes, the following is a working solution:
-(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
CGRect activeCellRect = [self.tableView rectForRowAtIndexPath:self.indexPath];
if ((activeCellRect.origin.y + activeCellRect.size.height) >
(self.view.frame.origin.y + self.view.frame.size.height))
{
// If a row ends up off screen after a rotation, bring it back
// on screen.
[self.tableView scrollToRowAtIndexPath:self.indexPath
atScrollPosition:UITableViewScrollPositionBottom
animated:YES];
}
}
Update 2, on repositioning the UIPopover: After the scroll command it is necessary to send a reloadData message to the table view. Then the rectForRowAtIndexPath: method will correctly report the new position of the cell (otherwise it will not, as it is not updated properly after the scroll-command)!
On orientation change try checking indexPathsForVisibleRows to see if your cell is visible and then using scrollToRowAtIndexPath if not.. something like:
-(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation{
if (![[self.tableView indexPathsForVisibleRows] containsObject:[self.tableView indexPathForSelectedRow]]) {
[self.tableView scrollToRowAtIndexPath:[self.tableView indexPathForSelectedRow] atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
}
}
You already know which row is selected, right? You also know when the device orientation changes, or at least you can know that, because there are UIViewController methods for that. You can get the rectangle for the selected row using UITableView's -rectForRowAtIndexPath: method, and it's easy to make sure that rectangle stays visible using UIScrollView's -scrollRectToVisible:animated: method, which UITableView inherits.

Resources