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.
Related
I have a UITableViewCell which contains a UICollectionView. The UITableViewCell also contains a UIPageControl. I want to change the dots as the UICollectionView is swiped. I am using
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
to get the current visible UICollectionViewCell. But my problem is that since the UICollectionView lies in UITableViewCell to fetch the reference to the collectionView I require the indexPAth of the current table view in which collection cell is being swiped. How can I do this?
I want to do this:
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
// Change the dots
CustomTableCell *tableCell = [self.currentTable cellForRowAtIndexPath:_tablecellIndex];
NSInteger currentIndex = tableCell.currentCollectionView.contentOffset.x / tableCell.currentCollectionView.frame.size.width;
tableCell.currentPageControl.currentPage = currentIndex;
}
But how do I get the _tablecellIndex?
I tried doing :
NSArray *indexes = [self.currentTable visibleCells];
_tablecellIndex = indexes[0];
But this is not always true as sometimes the table cells are displayed half and user is swiping second cell.
You need to ask the tableview itself, what indexpath a given cell has. You do that with this command :
[self.formTableView indexPathForCell:myCell];
The other problem in your case is that you are within the collection view on the cell, and not within the tableview itself. So theres a few ways to do that - one nice way is to set up a delegate on the cell that can access the tableview. Thats a bit involved, so heres an even simpler way (self in this case is the cell object):
UITableView *parentTableView = (UITableView*)self.superview;
NSIndexPath *cellIndexPath = [parentTableView indexPathForCell:self];
That should do the job.
I have a UICollectionView that i need to detect which cell is in view.
I already have the content offset already using the code below:
float scrollPoint = self.collectionView.contentOffset.y;
now where i am stuck is at finding which cells are at that point.
How do I find out which cell(s) are at that point.
Use - indexPathForItemAtPoint:(CGPoint)point:
NSIndexPath* path = [self.collectionView indexPathForItemAtPoint:contentOffset];
if (path) {
// Do stuff.
}
Documentation.
Actually, your contentOffset will give you the wrong cell. You probably just want to use the point 0, 0 or whatever to get the cell that's at the left/top of the scrollview.
I have a uitableview which contains cells. I am trying to figure it out how to adjust the z order of the cells. I am trying to make this:
But when I load the tableview I get this:
I've already tried [tableView sendSubViewToBack:cell]. Also when I scroll to the bottom and then return to the top the result is like in picture 1 but when I scroll down the result is like in picture 2. I appreciate your help.
static NSString *CellIdentifier = #"Cell";
NSString *row = [NSString stringWithFormat:#"%li",(long)indexPath.row];
GetMoreTableViewCell *cell = [_chapters dequeueReusableCellWithIdentifier:CellIdentifier];
NSMutableDictionary *deckCategory = [[_data valueForKey:key] valueForKey:row];
if (cell == nil) {
cell = [[GetMoreTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
[tableView bringSubviewToFront:cell];
if([[[_data valueForKey:key] valueForKey:[NSString stringWithFormat:#"%i",indexPath.row]] valueForKey:#"isDefault"] == [NSNumber numberWithBool:YES]) {
cell.addButton.hidden = YES;
}
cell.ttitle.text = [[[_data valueForKey:key] valueForKey:row] valueForKey:#"Name"];
cell.backgroundColor = [UIColor clearColor];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
CALayer * l = [cell.cellImage layer];
[l setMasksToBounds:YES];
[l setCornerRadius:10.0];
CALayer * b = [cell.cellBackground layer];
[b setMasksToBounds:YES];
[b setCornerRadius:19.0];
cell.cellImage.image = [UIImage imageNamed:[deckCategory valueForKey:#"Image"]];
if(indexPath.row == 0) {
cell.cellBackground.image = [UIImage imageNamed:#"card_green.png"];
}
else if(indexPath.row == 1) {
cell.cellBackground.image = [UIImage imageNamed:#"card_orange.png"];
}
else if(indexPath.row == 2) {
cell.cellBackground.image = [UIImage imageNamed:#"card_red.png"];
}
return cell;
I faced the same problem when using shadows on my UITableViewCells, the cells further down the screen (In the Y direction) had shadows overlapping from the higher ones at the top of the screen.
My solution was to set the Z Position on the layer of UITableViewCell based upon the indexPath.
cell.layer.zPosition = CGFloat(indexPath.row)
My issue was only occurring within individual sections. If your cells from subsequent sections also face this problem then you will need to add section number into the calculation.
cell.layer.zPosition = CGFloat(indexPath.section) * 1000.0 + CGFloat(indexPath.row)
If you have more than 1000 rows in each section then increase the magic number, or think about why you have 1000 rows in your App ;-)
It should work by changing the zPosition of the cell layer like this : cell.layer.zPosition = CGFloat(indexPath.row) in the tableView:cellForRowAtIndexPath: dataSource method. And don't forget to set the clipsToBound property to false for the cell.
I faced the same issue some time back.
The solution that worked for me is using a Collection view instead of a TableView. Create a custom class by extending it from UICollectionViewFlowLayout and use that in your Collection View instead.
class OverlappedCustomFlowLayout: UICollectionViewFlowLayout {
override func prepare() {
super.prepare()
// This allows us to make intersection and overlapping
// A negative number implies overlapping whereas positive implies space between the adjacent edges of two cells.
minimumLineSpacing = -100
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect)
for currentLayoutAttributes: UICollectionViewLayoutAttributes in layoutAttributes! {
// zIndex - Specifies the item’s position on the z-axis.
// Unlike a layer's zPosition, changing zIndex allows us to change not only layer position,
// but tapping/UI interaction logic too as it moves the whole item.
currentLayoutAttributes.zIndex = currentLayoutAttributes.indexPath.row + 1
}
}
return layoutAttributes
}
P.S. - As per Apple's developer document of zIndex
This property is used to determine the front-to-back ordering of items
during layout. Items with higher index values appear on top of items
with lower values. Items with the same value have an undetermined
order. The default value of this property is 0.
Hope it helps! :)
Unfortunately you can't control the order that cellForRow... is called in, so sending the cell to the back or the front isn't going to do it.
You would probably need to make a method which passed through the visible cells of the table and reordered them according to index path, but you're messing with views that you don't have control over. The table view can rearrange or add subviews at any time and you'd be trying to catch all of those events.
I would implement a view like this with a collection view rather than a table view, which would allow you to specify a z position for each indexPath using a custom layout, and would allow you to overlap the cells by a varying amount. Creating a custom layout is pretty simple, particularly for a table-type layout.
If you want to have descending zPositions (so that the first cell is on top), use the following code based on the answer from Brett. With this approach, there's no need to recalculate the zPosition value manually.
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
DispatchQueue.main.async {
for index in 0 ..< tableView.visibleCells.count {
let zPosition = CGFloat(tableView.visibleCells.count - index)
tableView.visibleCells[index].layer.zPosition = zPosition
}
}
}
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.
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.