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.
Related
I have a custom UITableViewCell which contains a UITextView. The cell is fairly large. How can I autoscroll the table to ensure the current selection point is always visible?
Note the UITextView is the size of the cell so it won't autoscroll itself.
I can call firstRectForRange on the selectedTextRange, but this doesn't work if the insertion point is at the end (you get inf,inf,0,0 for the CGRect). If I had the rect I could calculate the offset in the table and adjust its scroll contentOffset. is there a good way to do this or is messing with the contentOffset the only way?
use this in the textviewdidchange
if let confirmedTextViewCursorPosition = textView.selectedTextRange?.end {
let caretPosition = textView.caretRect(for: confirmedTextViewCursorPosition)
var textViewActualPosition = tableView.convert(caretPosition, from: textView.superview?.superview)
textViewActualPosition.origin.y += 20.0 // give the actual padding of textview inside the cell
tableView.scrollRectToVisible(textViewActualPosition, animated: false)
}
any luck on this? I',m having trouble with the exact same situation. What I',m currently doing is getting the rect of the caret position by using
[textView caretRectForPosition:textView.selectedTextRange.end]
then trying to compute from
convertRect: fromView:
I'm trying to make something similar to what UltraVisual for iOS already does. I'd like to make my pull-to-refresh be in a cell in-between other cells.
I think the following GIF animation explains it better:
It looks like the first cell fades out when pulling up, while when you pull down and you're at the top of the table, it adds a new cell right below the first one and use it as the pull-to-refresh.
Has anyone done anything similar?
Wrote this one for UV. Its actually way simpler than you're describing. Also, for what its worth, this view was written as a UICollectionView, but the logic still applies to UITableView.
There is only one header cell. Durring the 'refresh' animation, I simply set the content inset of the UICollectionView to hold it open. Then when I've finished with the reload, I animate the content inset back to the default value.
As for the springy fixed header, there's a couple of ways you can handle it. Quick and dirty is to use a UICollectionViewFlowLayout, and modify the attributes in - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
Here's some pseudo code assuming your first cell is the sticky header:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *layoutAttributes = [super layoutAttributesForElementsInRect:rect];
if ([self contentOffsetIsBelowZero]) {
for (UICollectionViewLayoutAttributes *attributes in layoutAttributes) {
if (attributes.indexPath.item == 0) {
CGPoint bottomOrigin = CGPointMake(0, CGRectGetMaxY(attributes.frame));
CGPoint converted = [self.collectionView convertPoint:bottomOrigin toView:self.collectionView.superview];
height = MAX(height, CGRectGetHeight(attributes.frame));
CGFloat offset = CGRectGetHeight(attributes.frame) - height;
attributes.frame = CGRectOffset(CGRectSetHeight(attributes.frame, height), 0, offset);
break;
}
}
}
Another approach would be to write a custom UICollectionViewLayout and calculate the CGRect's manually.
And finally, the 'fade out' is really nothing more than setting the opacity of the objects inside the first cell as it moves off screen. You can calculate the position of the cell on screen during - (void)applyLayoutAttributes… and set the opacity based on that.
Finally, something to note: In order to do any 'scroll based' updates with UICollectionView, you'll need to make sure - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds returns YES. You can do a simple optimisation check like:
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
BOOL shouldInvalidate = [super shouldInvalidateLayoutForBoundsChange:newBounds];
if ([self contentOffsetIsBelowZero]) {
shouldInvalidate = YES;
}
return shouldInvalidate;
}
Again this is mostly pseudo code, so re-write based on your own implementation. Hope this helps!
I have a UITableViewCell subclass which has an image, title and description.
I am supposed to resize the cell height according to the description content length i.e. if it spans more than 5 lines, I should extend it (+other subviews like image etc) till it lasts.
The next coming cells should begin only after that.
I have my UITableViewCell subclass instantiated from xib which has a fixed row height = 160.
I know this is pretty standard requirement but I am unable to find any guidelines.
I already extended layoutSubViews like this:
- (void) layoutSubviews
{
[self resizeCellImage];
}
- (void) resizeCellImage
{
CGRect descriptionRect = self.cellDescriptionLabel.frame;
CGRect imageRect = self.cellImageView.frame;
float descriptionBottomEdgeY = descriptionRect.origin.y + descriptionRect.size.height;
float imageBottomEdgeY = imageRect.origin.y + imageRect.size.height;
if (imageBottomEdgeY >= descriptionBottomEdgeY)
return;
//push the bottom of image to the bottom of description
imageBottomEdgeY = descriptionBottomEdgeY;
float newImageHeight = imageBottomEdgeY - imageRect.origin.y;
imageRect.size.height = newImageHeight;
self.cellImageView.frame = imageRect;
CGRect cellFrame = self.frame;
cellFrame.size.height = imageRect.size.height + imageRect.origin.y + 5;
CGRect contentFrame = self.contentView.frame;
contentFrame.size.height = cellFrame.size.height - 1;
self.contentView.frame = contentFrame;
self.frame = cellFrame;
}
It pretty much tells that if description is taller than image, we must resize the image as well as cell height to fit the description.
However when I invoke this code by doing this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
cell.cellDescriptionLabel.text = #"Some long string";
[cell.cellDescriptionLabel sizeToFit];
[cell setNeedsLayout];
return cell;
}
It appears that while cell frame is changed due to layoutSubViews call, other cells do not respect it. That is, they appear on the same position had the previous cell would not have resized itself.
Two questions:
How to make it possible what I want?
Am I doing right by calling setNeedsLayout within cellForRowAtIndexPath?
P.S.: I know heightForRowAtIndexPath holds key to changing the cell height, but I feel that the data parsing (not shown here) that I do as part of cellForRowAtIndexPath would be an overkill just to calculate height. I need something that can directly tell the UITableViewCell to resize itself according to content needs.
-tableView:heightForRowAtIndexPath: is by design how variable sized cells are calculated. The actual frame of a cell is of no importance and is changed by the table view to fit its needs.
You are sort of thinking of this backwards. The delegate tells the table view how cells need to be drawn, then the table view forces cells to fit those characteristics. The only thing you need to provide to the cell is the data it needs to hold.
This is because a table view calculates all the heights of all the cells before it has any cells to draw. This is done to allow a table view to size it's scroll view correctly. It allows for properly sized scroll bars and smooth quick-pans through the table view. Cells are only requested when a table view thinks a cell needs to be displayed to the screen.
UPDATE: How Do I Get Cell Heights
I've had to do this a couple of times. I have my view controller keep a cell which is never used in the table view.
#property (nonatomic) MyTableViewCell *standInCell;
I then use this cell as a stand in when I need measurements. I determine the base height of the cell without the variable sized views.
#property (nonatomic) CGFloat standInCellBaseHeight;
Then in -tableView:heightForRowAtIndexPath:, I get the height for all my variable sized views with the actual data for that index path. I add the variable sized heights to my stand in cell base height. I return that new calculated height.
Note, this is all non-autolayout. I'm sure the approach would be similar, but not identical to this, but I have no experience.
-tableView:heightForRowAtIndexPath: is the preferred way to tell tableview the size of its cells. You may either precalculate and cache it in a dictionary and reuse, or alternatively in ios7, you can use -tableView:estimatedHeightForRowAtIndexPath: to estimate the sizes.
Take a look at this thread - https://stackoverflow.com/questions/18746929/using-auto-layout-in-uitableview-for-dynamic-cell-layouts-variable-row-heights, the answer points to a very good example project here - https://github.com/caoimghgin/TableViewCellWithAutoLayout.
Sorry, but as far as I know you have to implement tableView:heightForRowAtIndexPath:. Warning, in iOS 6 this gets called on every row in you UITableView right away, I think to draw the scrollbar. iOS7 introduces tableView:estimatedHeightForRowAtIndexPath: which if implemented allows you to just guess at the height before doing all the calculation. This can help out a lot on very large tables.
What I found works well is just have your tableView:heightForRowAtIndexPath: call cellForRowAtIndexPath: to get the cell for that row, and then query that cell for it's height cell.bounds.size.height and return that.
This works pretty well for small tables or in iOS7 with tableView:estimatedHeightForRowAtIndexPath implemented.
I have a horizontal UITableView where I would like to set the paging distance. I tried this approach,
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
_tableView.decelerationRate = UIScrollViewDecelerationRateFast;
CGFloat pageSize = 320.f / 3.f;
CGPoint contentOffset = _tableView.contentOffset;
contentOffset.y = roundf(contentOffset.y / pageSize) * pageSize;
[_tableView setContentOffset:contentOffset animated:YES];
}
This works when you scroll slowly and let go but if you scroll fast, there's a lot of popping. So after wrestling with it for a bit, I'm trying a different approach...
I'm simply enabling paging on the tableView and then setting the width of the table to my desired paging size (a 3rd of the screen's width). I also set clipsToBounds = NO. When I use this approach, the scrolling works as expected but now cells outside of my smaller table width do not draw. What I would like is to force the cell on the left and right of my current cell to draw, even though they are outside of the UITableView's frame. I know cellForRowAtIndexPath get's called from a deeper level but is there some way I can trigger it myself for the cell's I selected?
I've tried using,
[[_tableView cellForRowAtIndexPath:indexPath] setNeedsDisplay];
but it does nothinggggggg!
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.