Programmatically scroll to a supplementary view within UICollectionView - iphone

I am using UICollectionView to display photos in sections. Each section has a supplementary view as a header and is supplied via the method: viewForSupplementaryElementOfKind.
I have a scrubber on the side that allows the user to jump from section to section. For now I am scrolling to the first item in the section using scrollToItemAtIndexPath:atScrollPosition:animated:, but what I really want is to scroll the collectionView so that that section's header is at the top of the screen, not the first cell. I do not see an obvious method to do this with. Do any of you have a work around?
I suppose I could scroll to the first item of the section, and then offset that by the supplementary height plus the offset between the items and header if it comes down to that (there is a method for scrolling to point coordinates of the contentView). However if there is a simpler way, I'd like to know.
Thanks.

You can not use scrollToItemAtIndexPath:atScrollPosition:animated for this.
Hopefully, they will add a new method like scrollToSupplementaryElementOfKind:atIndexPath: in the future, but for now, the only way is to manipulate the contentOffset directly.
The code below shows how to scroll header to be on top vertically with FlowLayout. You can do the same for horizontal scrolling, or use this idea for other layout types.
NSIndexPath *indexPath = ... // indexPath of your header, item must be 0
CGFloat offsetY = [collectionView layoutAttributesForSupplementaryElementOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath].frame.origin.y;
CGFloat contentInsetY = self.contentInset.top;
CGFloat sectionInsetY = ((UICollectionViewFlowLayout *)collectionView.collectionViewLayout).sectionInset.top;
[collectionView setContentOffset:CGPointMake(collectionView.contentOffset.x, offsetY - contentInsetY - sectionInsetY) animated:YES];
Note that if you have non-zero contentInset (as in iOS 7, when scroll views expand below bars) you need to subtract it from the offsetY, as shown. Same for sectionInset.
Update:
The code assumes that the layout is in prepared, "valid" state because it uses it to calculate the offset. The layout is prepared when the collection view presents its content.
The call to [_collectionView.collectionViewLayout prepareLayout] before the code above may help when you need to scroll the collection view which is not yet presented (from viewDidLoad say). The call to layoutIfNeeded (as #Vrasidas suggested in comments) should work too because it also prepares the layout.

Solution in Swift,
let section: Int = 0 // Top
if let cv = self.collectionView {
cv.layoutIfNeeded()
let indexPath = IndexPath(row: 1, section: section)
if let attributes = cv.layoutAttributesForSupplementaryElement(ofKind: UICollectionView.elementKindSectionHeader, at: indexPath) {
let topOfHeader = CGPoint(x: 0, y: attributes.frame.origin.y - cv.contentInset.top)
cv.setContentOffset(topOfHeader, animated: true)
}
}
Props to Gene De Lisa: http://www.rockhoppertech.com/blog/scroll-to-uicollectionview-header/

// scroll to selected index
NSIndexPath* cellIndexPath = [NSIndexPath indexPathForItem:0 inSection:sectionIndex];
UICollectionViewLayoutAttributes* attr = [self.collectionView.collectionViewLayout layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:cellIndexPath];
UIEdgeInsets insets = self.collectionView.scrollIndicatorInsets;
CGRect rect = attr.frame;
rect.size = self.collectionView.frame.size;
rect.size.height -= insets.top + insets.bottom;
CGFloat offset = (rect.origin.y + rect.size.height) - self.collectionView.contentSize.height;
if ( offset > 0.0 ) rect = CGRectOffset(rect, 0, -offset);
[self.collectionView scrollRectToVisible:rect animated:YES];

One thing the solutions don't manage is if you are pinning section headers. The methods work fine with unpinned headers, but if your headers are pinned, while scrolling to a section above the current section, it will stop once the section header appears (which will be for the bottom row of your section). That may be desirable in some cases but I think the goal is to put the top of the section at the top of the screen.
In which case you need to take the methods above and adjust them a bit. For instance:
UICollectionView *cv = self.collectionView;
CGFloat contentInsetY = cv.contentInset.top;
CGFloat offsetY = [cv layoutAttributesForItemAtIndexPath:ip].frame.origin.y;
CGFloat sectionHeight =
[cv layoutAttributesForSupplementaryElementOfKind:UICollectionElementKindSectionHeader atIndexPath:ip].frame.size.height;
[cv setContentOffset:CGPointMake(cv.contentOffset.x, offsetY - contentInsetY - sectionHeight) animated:YES];
Now you are basically scrolling the first row of your section to visible, less the height of its section header. This will put the section header on the top where you want it with pinned headers so the direction of the scroll won't matter anymore. I didn't test with section insets.

Related

set width of cell in grouped uitableview [duplicate]

I have been working on this for about 2 days, so i thought i share my learnings with you.
The question is: Is it possible to make the width of a cell in a grouped UITableView smaller?
The answer is: No.
But there are two ways you can get around this problem.
Solution #1: A thinner table
It is possible to change the frame of the tableView, so that the table will be smaller. This will result in UITableView rendering the cell inside with the reduced width.
A solution for this can look like this:
-(void)viewWillAppear:(BOOL)animated
{
CGFloat tableBorderLeft = 20;
CGFloat tableBorderRight = 20;
CGRect tableRect = self.view.frame;
tableRect.origin.x += tableBorderLeft; // make the table begin a few pixels right from its origin
tableRect.size.width -= tableBorderLeft + tableBorderRight; // reduce the width of the table
tableView.frame = tableRect;
}
Solution #2: Having cells rendered by images
This solution is described here: http://cocoawithlove.com/2009/04/easy-custom-uitableview-drawing.html
I hope this information is helpful to you. It took me about 2 days to try a lot of possibilities. This is what was left.
A better and cleaner way to achieve this is subclassing UITableViewCell and overriding its -setFrame: method like this:
- (void)setFrame:(CGRect)frame {
frame.origin.x += inset;
frame.size.width -= 2 * inset;
[super setFrame:frame];
}
Why is it better? Because the other two are worse.
Adjust table view width in -viewWillAppear:
First of all, this is unreliable, the superview or parent view controller may adjust table view frame further after -viewWillAppear: is called. Of course, you can subclass and override -setFrame: for your UITableView just like what I do here for UITableViewCells. However, subclassing UITableViewCells is a much common, light, and Apple way.
Secondly, if your UITableView have backgroundView, you don't want its backgroundView be narrowed down together. Keeping backgroundView width while narrow down UITableView width is not trivial work, not to mention that expanding subviews beyond its superview is not a very elegant thing to do in the first place.
Custom cell rendering to fake a narrower width
To do this, you have to prepare special background images with horizontal margins, and you have to layout subviews of cells yourself to accommodate the margins.
In comparison, if you simply adjust the width of the whole cell, autoresizing will do all the works for you.
To do this in Swift, which does not provide methods to set variables, you'll have to override the setter for frame. Originally posted (at least where I found it) here
override var frame: CGRect {
get {
return super.frame
}
set (newFrame) {
let inset: CGFloat = 15
var frame = newFrame
frame.origin.x += inset
frame.size.width -= 2 * inset
super.frame = frame
}
}
If nothing works you can try this
Make the background colour of the cell as clear color and then put an image of the cell with required size. If you want to display some text on that cell put a label above the image. Don't forget to set the background color of the label also to clear color.
I found the accepted solution didn't work upon rotation. To achieve UITableViewCells with fixed widths & flexible margins I just adapted the above solution to the following:
- (void)setFrame:(CGRect)frame {
if (self.superview) {
float cellWidth = 500.0;
frame.origin.x = (self.superview.frame.size.width - cellWidth) / 2;
frame.size.width = cellWidth;
}
[super setFrame:frame];
}
The method gets called whenever the device rotates, so the cells will always be centered.
There is a method that is called when the screen is rotated : viewWillTransitionToSize
This is where you should resize the frame. See example. Change the frame coords as you need to.
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[coordinator animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext> context)
{
int x = 0;
int y = 0;
self.tableView.frame = CGRectMake(x, y, 320, self.tableView.frame.size.height);
}];
}
i do it in
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell;
CGFloat tableBorderLeft = self.view.frame.origin.x + 10;
CGFloat tableBorderRight = self.view.frame.size.width - 20;
CGRect tableRect = self.view.frame;
tableRect.origin.x = tableBorderLeft;
tableRect.size.width = tableBorderRight;
tableView.frame = tableRect;
}
And this worked for me
In .h file add the delegate 'UITableViewDataSource'
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return size;
}

UICollectionViewCells with UICollectionViewFlowLayout - strange layout happening

I am currently testing in xcode a UICollectionView with just one horizontal row like a kinda cover flow. Basically I have my own Custom Cell class and xib file for the cell and then on each cell I am adding another UIView with a xib. In case you are wondering why, it is so I can add different UIViews to the cell. Right now I am only adding one.
Edit I have followed the WWDC 2012 video on creating a linelayout of a UICollectionViewCell with one difference. Instead of the cell in the middle getting bigger all the other cells get smaller.
Everything below is new to my question.
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *array = [super layoutAttributesForElementsInRect:rect];
CGRect visibleRect;
visibleRect.origin = self.collectionView.contentOffset;
visibleRect.size = self.collectionView.bounds.size;
for (UICollectionViewLayoutAttributes *attributes in array){
if (CGRectIntersectsRect(attributes.frame, rect)) {
CGFloat distance = CGRectGetMidX(visibleRect) - attributes.center.x;
CGFloat normalizedDistance = distance / ACTIVE_DISTANCE;
if (ABS(distance) < ACTIVE_DISTANCE) {
//THIS WOULD MAKE THE MIDDLE BIGGER
//CGFloat zoom = 1 + ZOOM_FACTOR *(1- ABS(normalizedDistance));
//attributes.transform3D = CATransform3DMakeScale(zoom, zoom, 1.0);
//attributes.zIndex = round(zoom);
} else {
//THIS MAKES ALL THE OTHERS NOT IN THE RECT SMALLER
CGFloat zoom = 1 + ZOOM_FACTOR *(1- ABS(normalizedDistance));
attributes.transform3D = CATransform3DMakeScale(zoom, zoom, 1.0);
attributes.zIndex = round(zoom);
}
}
}
return array;
}
The problem can be seen in the attached image.
Pink = Collection View Size
Brown = Cell size
Green = Cells Content size and an attached xib to the content size.
The problem I THINK I have is with the layout. When the sell is dequeued it is made smaller by the above code. Then when it is reused the CELL gets bigger again but the content view does not.
I have tired to manually set the frame of the content view but that does nothing.
UPDATE 1: This also only happens when I add a xib to the Cells content view. If there is no subview to the content view then there is no problem
UPDate 2: It appears that the subview of the cell, my xib is not resizing. I have tried to manually change its frame size but the only place this helps is in the cells drawrect method which feels like a hack to me.
reused cell not able to redraw itself so give call to
-(void)setFrame:(CGRect)frame {
[super setFrame:frame];
[self setNeedsDisplay]; // force drawRect:
}
from cellForItemAtIndexPath of the UICollectionView.
also have a look at this link
and this question
My answer is very specific and I am not sure it will help anyone.
The problem was that I had a constraint on the bottom of the grey view. After I changed this constraint to a less than or equal too then for some reason it worked.
Now I know this does not explain why it was not happening to every cell but it fixed my problem.
As such Harsh's answer might also be worth looking at if you have landed here after doing a search.
Edit: there also appears to be some bugs in the 6.0 UiCollectionView controller which seem to be fixed in 6.1

Large cells in a UICollectionView getting removed while the cell is still displayed

I have a UICollectionView that contains some large UICollectionViewCells. These cells are so large that they completely fill the UICollectionView bounds and extend off screen.
The problem is that the large cells are removed from the UICollectionView and queued for reuse while they are still displayed, resulting in a blank view. If I continue scrolling on the blank UICollectionView area, eventually the final portion of the cell appears and the start of the next cell appears in exactly the right place.
I've effectively disabled cell reuse and the problem still occurs apparently because the UICollectionView thinks that the cell is no longer displayed since no corner is within the bounds of the collection view.
To demonstrate make a collection view that is a single column and have a cell that is 10000px tall, when scrolling over the very tall cell it will disappear after about 1000px of content is scrolled off the screen and will reappear to display the final 1000px of content for the cell.
You can download a simple prototype app that displays this problem at: http://jack.cox.s3.amazonaws.com/collectionviewprototype.zip
This issue happened for me along with this warning in the debug log:
the behavior of the UICollectionViewFlowLayout is not defined because:
the item height must be less that the height of the UICollectionView
minus the section insets top and bottom values.
It seems that the out of the box UICollectionViewFlowLayout does not support cells larger than the screen.
This issue is being tracked as radar #12889164
Try this fix if you are still looking for a quick solution:
When a cell is detected to have a bigger height than the collectionview holding it, the collection view simply needs to be larger than the cell. So set the collecitonView frame to be bigger and correct the content and indicator insets.
- (void)updateCollectionViewForLargeCellHeight:(CGFloat)largeCellHeight {
CGFloat currentCollectionViewHeight = CGRectGetHeight(self.collectionView.frame);
if (largeCellHeight > currentCollectionViewHeight) {
self.collectionViewBottomConstraint = -largeCellHeight;
//This is the bottom constraint of the collectionView to its superview.
//If you are not using constraints, simply set the frame for the collectionView
UIEdgeInsets edgeInset = UIEdgeInsetsMake(0, 0, largeCellHeight, 0);
self.collectionView.contentInset = edgeInset;
self.collectionView.scrollIndicatorInsets = edgeInset;
[self.collectionView needsUpdateConstraints];
}
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
CGSize size;
[self updateCollectionViewForLargeCellHeight:size.height];
return size;
}
I have used UICollectionView with cells of the size of the screen. For iPad the cell size was 768x1024. And haven't found any issues. Please make sure that you have set the Min Spacing For Cells and For Lines are 0 Also return correct CGSize and UIEdgeInsets as follows
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
CGSize retval;
retval.width=maxWidth; //768 in case of iPad
retval.height=maxHeight; //1024 in case of iPad
return retval;
}
- (UIEdgeInsets)collectionView:
(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
return UIEdgeInsetsMake(0 , 0, 0, 0);
}

UITableView anchor rows to bottom

I have a UITableView that needs to introduce new content from the bottom. This is how a table view behaves when the view is full and you add new rows with animation.
I was starting it by altering the contentInset as rows are introduced but then when they change things go off, and the entry animation is all wrong... My problem with this approach is compounded by the fact that users can delete rows, and the row contents update, causing them to resize (each row has it's own height which changes).
Any recommendations on how to get a UITableView rows to always appear at the bottom of the UITableView's view space?
I've got a solution that works for me perfectly, but it causes a bunch of double thinking so it's not as simple in theory as it is in practice... kinda...
Step 1, apply a transform to the table view rotating it 180deg
tableView.transform = CGAffineTransformMakeRotation(-M_PI);
Step 2, rotate your raw cell 180deg in tableView:cellForRowAtIndexPath:
cell.transform = CGAffineTransformMakeRotation(M_PI);
Step 3, reverse your datasource. If you're using an NSMutableArray insert new objects at location 0 instead of using AddObject...
Now, the hard part is remembering that left is right and right is left only at the table level, so if you use
[tableView insertRowsAtIndexPaths:targetPath withRowAnimation:UITableViewRowAnimationLeft]
it now has to be
[tableView insertRowsAtIndexPaths:targetPath withRowAnimation:UITableViewRowAnimationRight]
and same for deletes, etc.
Depending on what your data store is you may have to handle that in reverse order as well...
Note: rotate the cells OPPOSITE the table, otherwise floating point innacuracy might cause the transform to get off perfect and you'll get crawlies on some graphics from time to time as you scroll... minor but annoying.
The accepted method introduces issues for my app - the scroll bar is on wrong side, and it mangles cell separators for UITableViewStyleGrouped
To fix this use the following
tableView.transform = CGAffineTransformMakeScale (1,-1);
and
cell.contentView.transform = CGAffineTransformMakeScale (1,-1);
// if you have an accessory view
cell.accessoryView.transform = CGAffineTransformMakeScale (1,-1);
Similar approach to ima747, but rotating 180 degrees also makes the scrolling indicator go to the opposite side. Instead I flipped the table view and its cells vertically.
self.tableView.transform = CGAffineTransformMakeScale(1, -1); //in viewDidLoad
cell.transform = CGAffineTransformMakeScale(1, -1);//in tableView:cellForRowAtIndexPath:
Create a table header that is the height of the screen (in whatever orientation you are in) LESS the height of the of rows you have that you want visible. If there are no rows, then the header is the full height of the table view. As rows are added, simultaneously reduce the height of the table header by the height of the new row. This means changing the height of the frame of the view you provide for the table header. The point is to fill the space above the table rows to give the appearance that the rows are entering from the bottom. Using a table header (or section header) pushes the table data down. You can put whatever you like in the header view, even have it blank and transparent if you like.
This should have the effect you are looking for, I think.
Look at the attribute tableHeaderView. You simply set this to the view you want displayed in the table header. Then you can manipulate it as needed as you add rows. I can't recall just how forceful you then need to be to get the view to actually update in the UI. Might be as simple as calling setNeedsDisplay, if anything.
Alternatively, look at the methods tableView:viewForHeaderInSection: and tableView:heightForHeaderInSection:. Similar to using a table header view, you would want to have an instance variable that you setup once but that you can access from these methods to return either the view itself or its height, respectively. When you need to change the view for the (first) section, you can use reloadSections:withAnimation: to force an update to the view on screen after you have changed the views height (or content).
Any of that make sense? I hope so. :-)
Swift 3.01 - Other solution can be, rotate and flip the table view. Works very well for me and not mess with the animation and is less work for the reload data on the table view.
self.tableView.transform = CGAffineTransform.init(rotationAngle: (-(CGFloat)(Double.pi)))
self.tableView.transform = CGAffineTransform.init(translationX: -view.frame.width, y: view.frame.height)
I just wanted to add something to all of these answers regarding the use of this technique with UICollectionView... Sometimes when invalidating the layout, my cells would get transformed back the wrong way, I found that in the UICollectionViewCell and UICollectionReusableView subclasses I had to do this:
- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
[super applyLayoutAttributes:layoutAttributes];
[self setTransform:CGAffineTransformMakeScale(1, -1)];
}
dataSourceArray = dataSourceArray.reversed()
tableView.transform = CGAffineTransform(scaleX: 1, y: -1)
cell.transform = CGAffineTransform(scaleX: 1, y: -1)
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if let text = textField.text {
dataSourceArray.insert(text, at: 0)
self.tableView.reloadData()
textField.text = ""
}
textField.resignFirstResponder()
return true
}
An easier way is to add the following lines at the bottom of cellForRowAtIndexPath
if(indexPath.section == self.noOfSections - 1)
[self scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:([self numberOfRowsInSection:self.noOfSections -1] - 1) inSection:self.noOfSections -1] atScrollPosition:UITableViewScrollPositionBottom animated:animated];
Late to the party but, inspired by #Sameh Youssef's idea, a function to scroll to the last cell in the tableview, assuming you only have one section. If not, just return the number of sections instead of hardcoding the 0.
The microsecond delay was arbitrarily chosen.
func scrollToLast() {
DispatchQueue.main.asyncAfter(deadline: .now() + .microseconds(5)) {
let lastIndex = IndexPath(row: self.tableView.numberOfRows(inSection: 0) - 1, section: 0)
if lastIndex.row != -1 {
self.tableView.scrollToRow(at: lastIndex, at: .bottom, animated: false)
}
}
}
I would recommend to use the approach described in this blog post.
Have checked it on iOS 12 and 13. Works perfectly.
Well, if you load your tableview with an NSMutableArray i would suggest you to sort out the array in the inverse order. So the table view will be filled up like you want.

Get a UITableView to scroll up from the bottom / snap to the bottom, not the top

I have a table view which is many cases will only have one or two cells that don't fill the screen. In this case, I would like the cells to sit at the bottom, rather than the top. In other words they should "snap" to the bottom of the tableview.
I can force the table view to scroll them to the bottom like this:
CGPoint bottomOffset = CGPointMake(0, [self.tableView contentSize].height - self.tableView.frame.size.height);
[self.tableView setContentOffset:bottomOffset animated:NO];
However, this is only partially successful. First, it doesn't work if I put it in viewDidLoad or viewWillAppear, only in viewDidAppear, which means that the user sees the tableview with the cells at the top first, then they move to the bottom. Second, if they scroll the table view, when they let go it automatically "snaps" back up to the top.
Does anyone know how to change this behaviour?
One option to consider is to resize the UITableView itself based on how many rows you will be displaying. Presuming that your UITableViewDelegate implements heightForRowAtIndexPath one can then set the height of the UITableView in a viewWillAppear method by multiplying the number of rows by the height of each row.
Something like this:
CGRect frame = [myTableView frame];
frame.size.height = [[myTableView dataSource] tableView: myTableView numberOfRowsInSection: 0] *
[[myTableView delegate] tableView: myTableView heightForRowAtIndexPath: [NSIndexPath indexPathForRow: 0 inSection: 0]];
[myTableView setFrame: frame];
This example assumes your table has one section and each row is the same height. Computing the size would have to get a little more complicated if multiple sections are involved or if different rows might be different heights.
Regardless of how the height is calculated the essence of the idea though is to just make the table itself shorter, no taller than the one or two rows that it is displaying, rather than trying to force it into behaving differently.