Scroll UICollectionView to section header view - ios

The code below scrolls to the right cell in UICollectionView, but the section header view is hidden behind UINavigationBar in my case. I believe I should use scrollRectToVisible instead and my question is, what is the right way to calculate the CGRect (y position) when the numberOfRows in a given section is variable.
- (void)scrollToPricing:(NSUInteger)row {
[self.collectionView scrollToItemAtIndexPath:
[NSIndexPath indexPathForItem:0
inSection:row]
atScrollPosition:UICollectionViewScrollPositionTop
animated:YES];
}

Seems like all the answers are overly complex. This works for me:
let attributes = self.collectionView.collectionViewLayout.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: NSIndexPath(forItem: 0, inSection: section))
self.collectionView.setContentOffset(CGPointMake(0, attributes!.frame.origin.y - self.collectionView.contentInset.top), animated: true)
Swift 3:
if let attributes = collectionView.collectionViewLayout.layoutAttributesForSupplementaryView(ofKind: UICollectionElementKindSectionHeader, at: IndexPath(item: 0, section: section)) {
collectionView.setContentOffset(CGPoint(x: 0, y: attributes.frame.origin.y - collectionView.contentInset.top), animated: true)
}

I think this may help you
UICollectionViewLayoutAttributes *attributes = [self.collectionView layoutAttributesForItemAtIndexPath:indexPath];
Then you can access the location through attributes.frame

Based on #pixelfreak answer
Swift 4.2 + iOS 11 Safe Area SUPPORT (for iPhone X and above)
if let attributes = collectionView.layoutAttributesForSupplementaryElement(ofKind: UICollectionView.elementKindSectionHeader, at: IndexPath(item: 0, section: section)) {
var offsetY = attributes.frame.origin.y - collectionView.contentInset.top
if #available(iOS 11.0, *) {
offsetY -= collectionView.safeAreaInsets.top
}
collectionView.setContentOffset(CGPoint(x: 0, y: offsetY), animated: true) // or animated: false
}
HAPPY CODING

First, get the frame for the header in the section:
- (CGRect)frameForHeaderForSection:(NSInteger)section {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:1 inSection:section];
UICollectionViewLayoutAttributes *attributes = [self.collectionView layoutAttributesForItemAtIndexPath:indexPath];
CGRect frameForFirstCell = attributes.frame;
CGFloat headerHeight = [self collectionView:_collectionView layout:_layout referenceSizeForHeaderInSection:section].height;
return CGRectOffset(frameForFirstCell, 0, -headerHeight);
}
Note: I just put a value of 1 for the indexPath.item. You might need to change this to something appropriate for your implementation.
Then, scroll the UIScrollView to the point at the top of the header:
- (void)scrollToTopOfSection:(NSInteger)section animated:(BOOL)animated {
CGRect headerRect = [self frameForHeaderForSection:section];
CGPoint topOfHeader = CGPointMake(0, headerRect.origin.y - _collectionView.contentInset.top);
[_collectionView setContentOffset:topOfHeader animated:animated];
}
Note: you must subtract the contentInset, otherwise it will be discarded and your scrollview will scroll behind the status bar and/or navigation bar.

// [myCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:index_of_sec] atScrollPosition:UICollectionViewScrollPositionTop animated:YES];
Below code will fix your problem, as i have faced same issue and used this solution developed by me instead of above commented code.
UICollectionViewLayoutAttributes *attributes = [myCollectionView layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:index_of_sec]];
CGRect rect = attributes.frame;
[myCollectionView setContentOffset:CGPointMake(myCollectionView.frame.origin.x, rect.origin.y - HEIGHT_OF_YOUR_HEADER) animated:YES];

The easiest way is to use the section header frame.origin.x and frame.origin.y and then add up section header height with the item height to create a CGRect to scroll to.
let sectionHeaderAttributes: UICollectionViewLayoutAttributes = self.collectionView.layoutAttributesForItem(at: scrollToIndexPath)!;
let itemAttributes: UICollectionViewLayoutAttributes = self.collectionView.layoutAttributesForSupplementaryElement(ofKind: UICollectionElementKindSectionHeader, at: scrollToIndexPath)!;
let combinedFrame: CGRect = CGRect(x: sectionHeaderAttributes.frame.origin.x, y: sectionHeaderAttributes.frame.origin.y, width: sectionHeaderAttributes.frame.width, height: sectionHeaderAttributes.frame.height + itemAttributes.frame.height);
collectionView.scrollRectToVisible(combinedFrame, animated: false);

// 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];

I found that the accepted answer no longer works properly (2 headers sticking together), especially when the collection view has to be scrolled up (down still works fine).
The following code works perfectly in either direction:
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
UICollectionViewLayoutAttributes *headerAttribs = [iconsCV layoutAttributesForSupplementaryElementOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath];
UICollectionViewLayoutAttributes *attribs = [iconsCV layoutAttributesForItemAtIndexPath:indexPath];
CGPoint topOfHeader = CGPointMake(0, attribs.frame.origin.y -collectionV.contentInset.top -headerAttribs.bounds.size.height);
[collectionV setContentOffset:topOfHeader animated:YES];

Faced a similar issue initially until I realized that the UICollectionView.ScrollPosition can be used to position the header in the collection view frame.
You just have to use the UICollectionViewScrollPositionCenteredVertically option for the header to be visible at the center of the collection view frame.
Here's how I have achieved this in Swift 4.2:
let scrollableSection = IndexPath.init(row: 0, section: indexPath.item)
emojiCollectionView.scrollToItem(at: scrollableSection, at: .centeredVertically, animated: true)

Related

How to vertical align (center) the content of UITableView?

I want to center the content of my UITableView that contains headerView and footerView created at storyboard, and UITableViewCells. How can I achieve this?
Here is what I'm trying to implement to solve my problem but this does not work.
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
CGFloat height = self.tableView.frameHeight - self.navigationController.navigationBar.frameHeight - [UIApplication sharedApplication].statusBarFrame.size.height - (self.rowCount * self.rowHeight);
self.tableView.tableHeaderView.frameHeight = height / 2.0;
}
So I subtracted the height of the navigationBar & statusBar and cells' height to the tableView's height to get the height of the empty area.
Now that I get the height if the empty area, I divided it to 2 for the footer and header's view.
In the viewWillAppear and in the didRotateFromInterfaceOrientation functions :
CGFloat headerHeight = (self.view.frame.size.height - (ROW_HEIGHT * [self.tableView numberOfRowsInSection:0]))) / 2;
self.tableView.contentInset = UIEdgeInsetsMake(headerHeight, 0, -headerHeight, 0);
This will solve your problem.
EDIT
Call the updateTableViewContentInset function in the viewWillLayoutSubviews and after each reloadData :
Ojective-C
- (void)updateTableViewContentInset {
CGFloat viewHeight = self.view.frame.size.height;
CGFloat tableViewContentHeight = self.tableView.contentSize.height;
CGFloat marginHeight = (viewHeight - tableViewContentHeight) / 2.0;
self.tableView.contentInset = UIEdgeInsetsMake(marginHeight, 0, -marginHeight, 0);
}
Swift 4
func updateTableViewContentInset() {
let viewHeight: CGFloat = view.frame.size.height
let tableViewContentHeight: CGFloat = tableView.contentSize.height
let marginHeight: CGFloat = (viewHeight - tableViewContentHeight) / 2.0
self.tableView.contentInset = UIEdgeInsets(top: marginHeight, left: 0, bottom: -marginHeight, right: 0)
}
Override viewDidLayoutSubviews and compare the tableView's frame with its contentSize to set its contentInset. In Swift 4 :
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let tableViewHeight = self.tableView.frame.height
let contentHeight = self.tableView.contentSize.height
let centeringInset = (tableViewHeight - contentHeight) / 2.0
let topInset = max(centeringInset, 0.0)
self.tableView.contentInset = UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0)
}
This works well with self-sizing table view cells 👌
Use the contentSize property of UITableView, that how you don't have to take the cell height or count into account.
- (void) updateTableInsetsForHeight: (CGFloat) height
{
CGFloat tableViewInset = MAX((height - self.tableView.contentSize.height) / 2.0, 0.f);
self.tableView.contentInset = UIEdgeInsetsMake(tableViewInset, 0, -tableViewInset, 0);
}
Update the contentInsets after reloading tableView-data, and when the containing view of the tableView becomes visible (viewWillAppear:) and when it changes size (viewWillTransitionToSize:withTransitionCoordinator:).
- (void) viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[self updateTableInsetsForHeight: size.height];
}
Swift 3 based on Pipiks answer
let headerHeight: CGFloat = (view.frame.size.height - CGFloat(Int(tableView.rowHeight) * tableView.numberOfRows(inSection: 0))) / 2
tableView.contentInset = UIEdgeInsetsMake(headerHeight, 0, -headerHeight, 0)
Add a UIView at the top (UIView should be parent of your TableView) of your TableView from storyboard. After that set its constraints to fill the view. Now, set the size of your TableView and set its constraints to center both vertically and horizontally your top
UIView. This will solve your problem.
I found out what causing my computation wrong. The value of self.tableView.bounds.size.height is different from the actual height in viewWillAppear. I used self.view.bounds.size.height instead.
for SWIFT 3 :
var headerHeight: CGFloat = (tableView.frame.size.height - CGFloat(Int(tableView.rowHeight) * tableView.numberOfRows(inSection: 0))) / 2
if headerHeight > 0 {
tableView.contentInset = UIEdgeInsetsMake(headerHeight, 0, 0/*-headerHeight*/, 0)
} else {
headerHeight = 0
tableView.contentInset = UIEdgeInsetsMake(headerHeight, 0, 0/*-headerHeight*/, 0)
}
RESULT :
Set frame of your UITableView like this
CGFloat width = self.view.frame.size.width;
CGFloat height = self.view.frame.size.height;
CGFloat tableHeight = 200.0; //your table height
self.tableView.frame = CGRectMake(0,(height-tableHeight)/2,width,tableHeight);
or try this

How to populate UITableView from the bottom upwards?

is it possible to reverse the order of a tableView. I have searched a lot for a solution but all the results have not quite been a solution to what I am trying to achieve. They all suggest scrolling to the last position of a table with scrollToRowAtIndexPath and populating the data in reverse. But this doesn't work if the table content is dynamic and in some instances not all the cells have data. For example in a normal tableView the order is:
label 1
label 2
label 3
empty
empty
scroll direction
v
V
the desired result would be:
scroll direction
^
^
empty
empty
empty
label 3
label 2
label 1
in this example if I used the suggested method of scrollToRowAtIndexPath and use the length of the array of objects, I would only get the third cell from the top. And end up with something like this:
unwanted outcome:
label 3
label 2
label 1
empty
empty
scroll direction
v
V
any help would be great thank you.
To populate UITableView from the bottom:
- (void)updateTableContentInset {
NSInteger numRows = [self.tableView numberOfRowsInSection:0];
CGFloat contentInsetTop = self.tableView.bounds.size.height;
for (NSInteger i = 0; i < numRows; i++) {
contentInsetTop -= [self tableView:self.tableView heightForRowAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
if (contentInsetTop <= 0) {
contentInsetTop = 0;
break;
}
}
self.tableView.contentInset = UIEdgeInsetsMake(contentInsetTop, 0, 0, 0);
}
To reverse the order of elements:
dataSourceArray = dataSourceArray.reverseObjectEnumerator.allObjects;
Swift 4.2/5 version:
func updateTableContentInset() {
let numRows = self.tableView.numberOfRows(inSection: 0)
var contentInsetTop = self.tableView.bounds.size.height
for i in 0..<numRows {
let rowRect = self.tableView.rectForRow(at: IndexPath(item: i, section: 0))
contentInsetTop -= rowRect.size.height
if contentInsetTop <= 0 {
contentInsetTop = 0
break
}
}
self.tableView.contentInset = UIEdgeInsets(top: contentInsetTop,left: 0,bottom: 0,right: 0)
}
Swift 3/4.0 version:
self.tableView.contentInset = UIEdgeInsetsMake(contentInsetTop, 0, 0, 0)
first reverse uitableview
tableView.transform = CGAffineTransformMakeScale (1,-1);
then reverse cell in cell create.
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
...
cell.contentView.transform = CGAffineTransformMakeScale (1,-1);
Swift 4.0 and 4.2 version
First reverse UITableView in viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
tableView.transform = CGAffineTransform(scaleX: 1, y: -1)
}
Then reverse the cell in cellForRowAt.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "MyTableViewCell", for: indexPath) as? MyTableViewCell else { fatalError() }
cell.contentView.transform = CGAffineTransform(scaleX: 1, y: -1)
return cell
}
Here is a refined solution of KlimczakM´s solution that works with autolayouted tableview cells (as well as the fixed ones). This solution also works with sections, section headers and section footers.
Swift 3.0:
func updateTableContentInset(forTableView tv: UITableView) {
let numSections = tv.numberOfSections
var contentInsetTop = tv.bounds.size.height -
(self.navigationBar?.frame.size.height ?? 0)
for section in 0..<numSections {
let numRows = tv.numberOfRows(inSection: section)
let sectionHeaderHeight = tv.rectForHeader(inSection: section).size.height
let sectionFooterHeight = tv.rectForFooter(inSection: section).size.height
contentInsetTop -= sectionHeaderHeight + sectionFooterHeight
for i in 0..<numRows {
let rowHeight = tv.rectForRow(at: IndexPath(item: i, section: section)).size.height
contentInsetTop -= rowHeight
if contentInsetTop <= 0 {
contentInsetTop = 0
break
}
}
// Break outer loop as well if contentInsetTop == 0
if contentInsetTop == 0 {
break
}
}
tv.contentInset = UIEdgeInsetsMake(contentInsetTop, 0, 0, 0)
}
NOTE:
Above code is untested but should work.
Just make sure that you cope for the height of any navbar or tabbar and you'll be fine. In the code above i only do that for the navbar!
Don't bother to write the code by yourself.
Why don't you use ReverseExtension. Its very easy and will give you all required results. Please follow this url
https://github.com/marty-suzuki/ReverseExtension
Note: Whenever you need to add a new cell, please insert newly added model at zeroth index of datasource array, so new cell should add at bottom. Otherwise it would add the cell at top and you would get confused again.
The simple way is use like UILabel multiple lines + autolayout.
-> UITableView should resize it base on it content(aka intrinsic layout).
Create your tableview and set base class following:
class IntrinsicTableView: UITableView {
override var contentSize:CGSize {
didSet {
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
self.layoutIfNeeded()
return CGSize(width: UIViewNoIntrinsicMetric, height: contentSize.height)
}
}
Now set left right and bottom layout constraints for your tableview pin to it parent view.
The most important is top layout constraint should set to great than or equal 0, This condition guaranteed table will not tall than it parent view.
I did in cellForRowAt method:
let reverseIndex = myArray.count-indexPath.row-1
let currCellData = myArray.object(at: reverseIndex)
and then you continue working with currCellData
//Add these lines where you want to reload your tableView
let indexpath = IndexPath(row: self.Array.count-1, section: 0)
self.tableView.scrollToRow(at: indexpath, at: .top, animated: true)
self.updateTableContentInset()
//Add this function below
func updateTableContentInset() {
let numRows = self.tableView.numberOfRows(inSection: 0)
var contentInsetTop = self.tableView.bounds.size.height
for i in 0..<numRows {
let rowRect = self.tableView.rectForRow(at: IndexPath(item: i, section: 0))
contentInsetTop -= rowRect.size.height
if contentInsetTop <= 0 {
contentInsetTop = 0
break
}
}
self.tableView.contentInset = UIEdgeInsets(top: contentInsetTop,left: 0,bottom: 0,right: 0)
}
I you want that every new cell should appear from the bottom of the tableView, use this:-
Invert the tableView:
myTableView.transform = CGAffineTransform (scaleX: -1,y: -1)
Invert the cells also:
cell.contentView.transform = CGAffineTransform (scaleX: -1,y: -1)
Now populate your tabelViewDataSource in opposite direction, like if you are using an array, then you may do like this:
myTableViewData.insert(<Your New Array Element>), at: 0)
This solution adjust the content inset as the content size changes using KVO. It also takes the content inset into account when scrolling to top as simply scrolling to CGPointZero will scroll to the top of the content instead of scrolling to the top of the table.
-(void)startObservingContentSizeChanges
{
[self.tableView addObserver:self forKeyPath:kKeyPathContentSize options:0 context:nil];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if([keyPath isEqualToString:kKeyPathContentSize] && object == self.tableView)
{
// difference between content and table heights. +1 accounts for last row separator
CGFloat height = MAX(self.tableView.frame.size.height - self.tableView.contentSize.height, 0) + 1;
self.tableView.contentInset = UIEdgeInsetsMake(height, 0, 0, 0);
// "scroll" to top taking inset into account
[self.tableView setContentOffset:CGPointMake(0, -height) animated:NO];
}
}
Swift 3.01 - Other solution can be, rotate and flip the tableView.
self.tableView.transform = CGAffineTransform.init(rotationAngle: (-(CGFloat)(M_PI)))
self.tableView.transform = CGAffineTransform.init(translationX: -view.frame.width, y: view.frame.height)
UITableView anchor rows to bottom
If you have an array of object you display in cellForRowAtIndexPath:, lets say dataArray you can reverse it, or in cellForRowAtIndexPath:
you can do something like that:
NSString *yourObject = dataArray[[dataArray count] - 1 - indexPath.row];
cell.textLabel.text = yourObject
I assume you keep strings in dataArray.

Resize UITableView height, and keep scrolled at bottom

Im trying to resize the tableview's height with a animation, it works fine by animating the tableview's frame.size.height.
The problem is, i have a tableview that is 200px height and scrolled to the bottom, i want to animate this to 100px, i run a simple
[UIView animateWithDuration:0.245f animations:^{
CGRect frame = tableview.frame;
frame.size.height = 100.f;
tableview.frame = frame;
}];
this works fine, but after i resized it it is no longer scrolled to the bottom of the tableview. i want the tableview to always be scrolled at the bottom while animating. i tried alot of differnet things like calling
[tablview scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
right before/after i start the resize animation, but i have not managed to get them to sync 100%. Is there a way to resize the tableview while the bottom of the tableview displays the last element of the scrollview.
I found a solution for my problem, there might very well be a better solution but this actually works quite well :)
Before i start animating i see if the contentSize.height is larger than the target height, if so i do following:
if (mMessages.contentSize.height > 150.f)
{
CGFloat expandedOffsetY = 52.f;
CGFloat collapsedBottomOffset = (150.f + 18.f);
CGFloat expandedBottomOffset = (MIN(320.f, mMessages.contentSize.height) + expandedOffsetY);
tableFrame.origin.y = (collapsedBottomOffset - expandedBottomOffset) + expandedOffsetY;
}
else
{
tableFrame.origin.y = 18.f;
tableFrame.size.height = 150.f;
}
this will put the tableview in a minus origin.y, then i have wrapped the tableview inside a "parent" uiview with clip sub views = YES.
Then i have a "completed" animation block that "resets" to the target values.
CGRect tableFrame = mMessages.frame;
tableFrame.origin.y = 18.f;
tableFrame.size.height = 150.f;
mMessages.frame = tableFrame;
if (mMessages.contentSize.height > tableFrame.size.height)
{
float contentOffsetY = mMessages.contentSize.height - tableFrame.size.height;
mMessages.contentOffset = CGPointMake(0.0f, contentOffsetY);
}
Try updating the contentOffset of the table view as you animate the frame.
[UIView animateWithDuration:0.245f animations:^{
CGRect frame = tableview.frame;
frame.size.height = 100.f;
tableview.frame = frame;
float contentOffsetY = tableview.contentSize - tableview.frame.size.height;
tableview.contentOffset = CGPointMake(0, contentOffsetY);
}];
[UIView animateWithDuration:0.245f animations:^{
CGRect frame = tableview.frame;
frame.size.height = 100.f;
tableview.frame = frame;
} completion:^(BOOL finished) {
// Find your last indexpath
[tablview scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
} ];
Scrolling the tableview to the bottom in the animation block was working for me, with one little twist: when I reduced the hight of the tableView I called scrollToIndexPath without animating, and when I expand the tableView I called the scrollToIndexPath with animation.
UIView.animate(withDuration: 0.25) { [weak self] in
guard let strongSelf = self
else { return }
// ...
// updateConstraints
// ...
strongSelf.view.layoutIfNeeded()
if reduceSize {
tableView.scrollToLastRow(animated: false)
} else {
tableView.scrollToLastRow(animated: true)
}
}
Extension for UITableView
extension UITableView {
func scrollToLastRow(animated: Bool) {
let lastSection = numberOfSections-1
let lastRow = numberOfRows(inSection: lastSection)-1
let lastIndexPath = IndexPath(row: lastRow, section: lastSection)
scrollToRow(at: lastIndexPath, at: .bottom, animated: animated)
}
}

How to know when UITableView did scroll to bottom in iPhone?

I would like to know when a UITableView did scroll to bottom in order to load and show more content, something like a delegate or something else to let the controller know when the table did scroll to bottom.
How can I do this?
in the tableview delegate do something like this
ObjC:
- (void)scrollViewDidScroll:(UIScrollView *)aScrollView {
CGPoint offset = aScrollView.contentOffset;
CGRect bounds = aScrollView.bounds;
CGSize size = aScrollView.contentSize;
UIEdgeInsets inset = aScrollView.contentInset;
float y = offset.y + bounds.size.height - inset.bottom;
float h = size.height;
// NSLog(#"offset: %f", offset.y);
// NSLog(#"content.height: %f", size.height);
// NSLog(#"bounds.height: %f", bounds.size.height);
// NSLog(#"inset.top: %f", inset.top);
// NSLog(#"inset.bottom: %f", inset.bottom);
// NSLog(#"pos: %f of %f", y, h);
float reload_distance = 10;
if(y > h + reload_distance) {
NSLog(#"load more rows");
}
}
Swift:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset
let bounds = scrollView.bounds
let size = scrollView.contentSize
let inset = scrollView.contentInset
let y = offset.y + bounds.size.height - inset.bottom
let h = size.height
let reload_distance:CGFloat = 10.0
if y > (h + reload_distance) {
print("load more rows")
}
}
Modified neoneyes answer a bit.
This answer targets those of you who only wants the event to be triggered once per release of the finger.
Suitable when loading more content from some content provider (web service, core data etc).
Note that this approach does not respect the response time from your web service.
- (void)scrollViewDidEndDragging:(UIScrollView *)aScrollView
willDecelerate:(BOOL)decelerate
{
CGPoint offset = aScrollView.contentOffset;
CGRect bounds = aScrollView.bounds;
CGSize size = aScrollView.contentSize;
UIEdgeInsets inset = aScrollView.contentInset;
float y = offset.y + bounds.size.height - inset.bottom;
float h = size.height;
float reload_distance = 50;
if(y > h + reload_distance) {
NSLog(#"load more rows");
}
}
add this method in the UITableViewDelegate:
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGFloat height = scrollView.frame.size.height;
CGFloat contentYoffset = scrollView.contentOffset.y;
CGFloat distanceFromBottom = scrollView.contentSize.height - contentYoffset;
if(distanceFromBottom < height)
{
NSLog(#"end of the table");
}
}
None of the answers above helped me, so I came up with this:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)aScrollView
{
NSArray *visibleRows = [self.tableView visibleCells];
UITableViewCell *lastVisibleCell = [visibleRows lastObject];
NSIndexPath *path = [self.tableView indexPathForCell:lastVisibleCell];
if(path.section == lastSection && path.row == lastRow)
{
// Do something here
}
}
The best way is to test a point at the bottom of the screen and use this method call when ever the user scrolls (scrollViewDidScroll):
- (NSIndexPath *)indexPathForRowAtPoint:(CGPoint)point
Test a point near the bottom of the screen, and then using the indexPath it returns check if that indexPath is the last row then if it is, add rows.
Use – tableView:willDisplayCell:forRowAtIndexPath: (UITableViewDelegate method)
Simply compare the indexPath with the items in your data array (or whatever data source you use for your table view) to figure out if the last element is being displayed.
Docs: http://developer.apple.com/library/ios/documentation/uikit/reference/UITableViewDelegate_Protocol/Reference/Reference.html#//apple_ref/occ/intfm/UITableViewDelegate/tableView:willDisplayCell:forRowAtIndexPath:
UITableView is a subclass of UIScrollView, and UITableViewDelegate conforms to UIScrollViewDelegate. So the delegate you attach to the table view will get events such as scrollViewDidScroll:, and you can call methods such as contentOffset on the table view to find the scroll position.
NSLog(#"%f / %f",tableView.contentOffset.y, tableView.contentSize.height - tableView.frame.size.height);
if (tableView.contentOffset.y == tableView.contentSize.height - tableView.frame.size.height)
[self doSomething];
Nice and simple
in Swift you can do something like this. Following condition will be true every time you reach end of the tableview
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.row+1 == postArray.count {
println("came to last row")
}
}
Building on #Jay Mayu's answer, which I felt was one of the better solutions:
Objective-C
// UITableViewDelegate
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
// Need to call the service & update the array
if(indexPath.row + 1 == self.sourceArray.count) {
DLog(#"Displayed the last row!");
}
}
Swift 2.x
// UITableViewDelegate
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if (indexPath.row + 1) == sourceArray.count {
print("Displayed the last row!")
}
}
Here is the swift 3.0 version code.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset
let bounds = scrollView.bounds
let size = scrollView.contentSize
let inset = scrollView.contentInset
let y: Float = Float(offset.y) + Float(bounds.size.height) + Float(inset.bottom)
let height: Float = Float(size.height)
let distance: Float = 10
if y > height + distance {
// load more data
}
}
I generally use this to load more data , when last cell starts display
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row == myDataArray.count-1) {
NSLog(#"load more");
}
}
Taking neoneye excellent answers but in swift, and renaming of the variables..
Basically we know we have reached the bottom of the table view if the yOffsetAtBottom is beyond the table content height.
func isTableViewScrolledToBottom() -> Bool {
let tableHeight = tableView.bounds.size.height
let contentHeight = tableView.contentSize.height
let insetHeight = tableView.contentInset.bottom
let yOffset = tableView.contentOffset.y
let yOffsetAtBottom = yOffset + tableHeight - insetHeight
return yOffsetAtBottom > contentHeight
}
My solution is to add cells before tableview will decelerate on estimated offset. It's predictable on by velocity.
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)offset {
NSLog(#"offset: %f", offset->y+scrollView.frame.size.height);
NSLog(#"Scroll view content size: %f", scrollView.contentSize.height);
if (offset->y+scrollView.frame.size.height > scrollView.contentSize.height - 300) {
NSLog(#"Load new rows when reaches 300px before end of content");
[[DataManager shared] fetchRecordsNextPage];
}
}
Update for Swift 3
Neoneye's answer worked best for me in Objective C, this is the equivalent of the answer in Swift 3:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let offset: CGPoint = scrollView.contentOffset
let bounds: CGRect = scrollView.bounds
let size: CGSize = scrollView.contentSize
let inset: UIEdgeInsets = scrollView.contentInset
let y: CGFloat = offset.y + bounds.size.height - inset.bottom
let h: CGFloat = size.height
// print("offset: %f", offset.y)
// print("content.height: %f", size.height)
// print("bounds.height: %f", bounds.size.height)
// print("inset.top: %f", inset.top)
// print("inset.bottom: %f", inset.bottom)
// print("position: %f of %f", y, h)
let reloadDistance: CGFloat = 10
if (y > h + reloadDistance) {
print("load more rows")
}
}
I want perform some action on my any 1 full Tableviewcell.
So the code is link the :
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
NSArray* cells = self.tableView.visibleCells;
NSIndexPath *indexPath = nil ;
for (int aIntCount = 0; aIntCount < [cells count]; aIntCount++)
{
UITableViewCell *cell = [cells objectAtIndex:aIntCount];
CGRect cellRect = [self.tableView convertRect:cell.frame toView:self.tableView.superview];
if (CGRectContainsRect(self.tableView.frame, cellRect))
{
indexPath = [self.tableView indexPathForCell:cell];
// remain logic
}
}
}
May this is help to some one.
#neoneye's answer worked for me. Here is the Swift 4 version of it
let offset = scrollView.contentOffset
let bounds = scrollView.bounds
let size = scrollView.contentSize
let insets = scrollView.contentInset
let y = offset.y + bounds.size.height - insets.bottom
let h = size.height
let reloadDistance = CGFloat(10)
if y > h + reloadDistance {
//load more rows
}

UIScrollView scroll to bottom programmatically

How can I make a UIScrollView scroll to the bottom within my code? Or in a more generic way, to any point of a subview?
You can use the UIScrollView's setContentOffset:animated: function to scroll to any part of the content view. Here's some code that would scroll to the bottom, assuming your scrollView is self.scrollView:
Objective-C:
CGPoint bottomOffset = CGPointMake(0, self.scrollView.contentSize.height - self.scrollView.bounds.size.height + self.scrollView.contentInset.bottom);
[self.scrollView setContentOffset:bottomOffset animated:YES];
Swift:
let bottomOffset = CGPoint(x: 0, y: scrollView.contentSize.height - scrollView.bounds.height + scrollView.contentInset.bottom)
scrollView.setContentOffset(bottomOffset, animated: true)
Swift version of the accepted answer for easy copy pasting:
let bottomOffset = CGPoint(x: 0, y: scrollView.contentSize.height - scrollView.bounds.size.height)
scrollView.setContentOffset(bottomOffset, animated: true)
Simplest Solution:
[scrollview scrollRectToVisible:CGRectMake(scrollview.contentSize.width - 1,scrollview.contentSize.height - 1, 1, 1) animated:YES];
A swifty implementation:
extension UIScrollView {
func scrollToBottom(animated: Bool) {
if self.contentSize.height < self.bounds.size.height { return }
let bottomOffset = CGPoint(x: 0, y: self.contentSize.height - self.bounds.size.height)
self.setContentOffset(bottomOffset, animated: animated)
}
}
use it:
yourScrollview.scrollToBottom(animated: true)
Just an enhancement to the existing answer.
CGPoint bottomOffset = CGPointMake(0, self.scrollView.contentSize.height - self.scrollView.bounds.size.height + self.scrollView.contentInset.bottom);
[self.scrollView setContentOffset:bottomOffset animated:YES];
It takes care of the bottom inset as well (in case you're using that to adjust your scroll view when the keyboard is visible)
Setting the content offset to the height of the content size is wrong: it scrolls the bottom of the content to the top of the scroll view, and thus out of sight.
The correct solution is to scroll the bottom of the content to the bottom of the scroll view, like this (sv is the UIScrollView):
CGSize csz = sv.contentSize;
CGSize bsz = sv.bounds.size;
if (sv.contentOffset.y + bsz.height > csz.height) {
[sv setContentOffset:CGPointMake(sv.contentOffset.x,
csz.height - bsz.height)
animated:YES];
}
A Swift 2.2 solution, taking contentInset into account
let bottomOffset = CGPoint(x: 0, y: scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.bottom)
scrollView.setContentOffset(bottomOffset, animated: true)
This should be in an extension
extension UIScrollView {
func scrollToBottom() {
let bottomOffset = CGPoint(x: 0, y: contentSize.height - bounds.size.height + contentInset.bottom)
setContentOffset(bottomOffset, animated: true)
}
}
Note that you may want to check if bottomOffset.y > 0 before scroll
What if contentSize is lower than bounds?
For Swift it is:
scrollView.setContentOffset(CGPointMake(0, max(scrollView.contentSize.height - scrollView.bounds.size.height, 0) ), animated: true)
Scroll To Top
- CGPoint topOffset = CGPointMake(0, 0);
- [scrollView setContentOffset:topOffset animated:YES];
Scroll To Bottom
- CGPoint bottomOffset = CGPointMake(0, scrollView.contentSize.height - self.scrollView.bounds.size.height);
- [scrollView setContentOffset:bottomOffset animated:YES];
It looks like all of the answers here didn't take the safe area into consideration.
Since iOS 11, iPhone X had a safe area introduced. This may affect the scrollView's contentInset.
For iOS 11 and above, to properly scroll to the bottom with the content inset included. You should use adjustedContentInset instead of contentInset. Check this code:
Swift:
let bottomOffset = CGPoint(x: 0, y: scrollView.contentSize.height - scrollView.bounds.height + scrollView.adjustedContentInset.bottom)
scrollView.setContentOffset(bottomOffset, animated: true)
Objective-C
CGPoint bottomOffset = CGPointMake(0, self.scrollView.contentSize.height - self.scrollView.bounds.size.height + self.scrollView.adjustedContentInset.bottom);
[self.scrollView setContentOffset:bottomOffset animated:YES];
Swift extension (this keeps the original contentOffset.x):
extension UIScrollView {
func scrollsToBottom(animated: Bool) {
let bottomOffset = CGPoint(x: contentOffset.x,
y: contentSize.height - bounds.height + adjustedContentInset.bottom)
setContentOffset(bottomOffset, animated: animated)
}
}
References:
adjustedContentInset
I also found another useful way of doing this in the case you are using a UITableview (which is a subclass of UIScrollView):
[(UITableView *)self.view scrollToRowAtIndexPath:scrollIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
Using UIScrollView's setContentOffset:animated: function to scroll to the bottom in Swift.
let bottomOffset : CGPoint = CGPointMake(0, scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.bottom)
scrollView.setContentOffset(bottomOffset, animated: true)
If you somehow change scrollView contentSize (ex. add something to stackView which is inside scrollView) you must call scrollView.layoutIfNeeded() before scrolling, otherwise it does nothing.
Example:
scrollView.layoutIfNeeded()
let bottomOffset = CGPoint(x: 0, y: scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.bottom)
if(bottomOffset.y > 0) {
scrollView.setContentOffset(bottomOffset, animated: true)
}
With an (optional) footerView and contentInset, the solution is:
CGPoint bottomOffset = CGPointMake(0, _tableView.contentSize.height - tableView.frame.size.height + _tableView.contentInset.bottom);
if (bottomOffset.y > 0) [_tableView setContentOffset: bottomOffset animated: YES];
Swift:
You could use an extension like this:
extension UIScrollView {
func scrollsToBottom(animated: Bool) {
let bottomOffset = CGPoint(x: 0, y: contentSize.height - bounds.size.height)
setContentOffset(bottomOffset, animated: animated)
}
}
Use:
scrollView.scrollsToBottom(animated: true)
valdyr, hope this will help you:
CGPoint bottomOffset = CGPointMake(0, [textView contentSize].height - textView.frame.size.height);
if (bottomOffset.y > 0)
[textView setContentOffset: bottomOffset animated: YES];
Category to the rescue!
Add this to a shared utility header somewhere:
#interface UIScrollView (ScrollToBottom)
- (void)scrollToBottomAnimated:(BOOL)animated;
#end
And then to that utility implementation:
#implementation UIScrollView(ScrollToBottom)
- (void)scrollToBottomAnimated:(BOOL)animated
{
CGPoint bottomOffset = CGPointMake(0, self.contentSize.height - self.bounds.size.height);
[self setContentOffset:bottomOffset animated:animated];
}
#end
Then Implement it wherever you like, for instance:
[[myWebView scrollView] scrollToBottomAnimated:YES];
For Horizontal ScrollView
If you like me has a Horizontal ScrollView and want to scroll to end of it (in my case to most right of it), you need to change some parts of the accepted answer:
Objective-C
CGPoint rightOffset = CGPointMake(self.scrollView.contentSize.width - self.scrollView.bounds.size.width + self.scrollView.contentInset.right, 0 );
[self.scrollView setContentOffset:rightOffset animated:YES];
Swift
let rightOffset: CGPoint = CGPoint(x: self.scrollView.contentSize.width - self.scrollView.bounds.size.width + self.scrollView.contentInset.right, y: 0)
self.scrollView.setContentOffset(rightOffset, animated: true)
A good way to ensure the bottom of your content is visible is to use the formula:
contentOffsetY = MIN(0, contentHeight - boundsHeight)
This ensures the bottom edge of your content is always at or above the bottom edge of the view. The MIN(0, ...) is required because UITableView (and probably UIScrollView) ensures contentOffsetY >= 0 when the user tries to scroll by visibly snapping contentOffsetY = 0. This looks pretty weird to the user.
The code to implement this is:
UIScrollView scrollView = ...;
CGSize contentSize = scrollView.contentSize;
CGSize boundsSize = scrollView.bounds.size;
if (contentSize.height > boundsSize.height)
{
CGPoint contentOffset = scrollView.contentOffset;
contentOffset.y = contentSize.height - boundsSize.height;
[scrollView setContentOffset:contentOffset animated:YES];
}
If you don't need animation, this works:
[self.scrollView setContentOffset:CGPointMake(0, CGFLOAT_MAX) animated:NO];
While Matt solution seems correct to me you need to take in account also the collection view inset if there is one that has been set-up.
The adapted code will be:
CGSize csz = sv.contentSize;
CGSize bsz = sv.bounds.size;
NSInteger bottomInset = sv.contentInset.bottom;
if (sv.contentOffset.y + bsz.height + bottomInset > csz.height) {
[sv setContentOffset:CGPointMake(sv.contentOffset.x,
csz.height - bsz.height + bottomInset)
animated:YES];
}
In swift:
if self.mainScroll.contentSize.height > self.mainScroll.bounds.size.height {
let bottomOffset = CGPointMake(0, self.mainScroll.contentSize.height - self.mainScroll.bounds.size.height);
self.mainScroll.setContentOffset(bottomOffset, animated: true)
}
Solution to scroll to last item of a table View :
Swift 3 :
if self.items.count > 0 {
self.tableView.scrollToRow(at: IndexPath.init(row: self.items.count - 1, section: 0), at: UITableViewScrollPosition.bottom, animated: true)
}
Didn't work for me, when I tried to use it in UITableViewController on self.tableView (iOS 4.1), after adding footerView. It scrolls out of the borders, showing black screen.
Alternative solution:
CGFloat height = self.tableView.contentSize.height;
[self.tableView setTableFooterView: myFooterView];
[self.tableView reloadData];
CGFloat delta = self.tableView.contentSize.height - height;
CGPoint offset = [self.tableView contentOffset];
offset.y += delta;
[self.tableView setContentOffset: offset animated: YES];
CGFloat yOffset = scrollView.contentOffset.y;
CGFloat height = scrollView.frame.size.height;
CGFloat contentHeight = scrollView.contentSize.height;
CGFloat distance = (contentHeight - height) - yOffset;
if(distance < 0)
{
return ;
}
CGPoint offset = scrollView.contentOffset;
offset.y += distance;
[scrollView setContentOffset:offset animated:YES];
I found that contentSize doesn't really reflect the actual size of the text, so when trying to scroll to the bottom, it will be a little bit off. The best way to determine the actual content size is actually to use the NSLayoutManager's usedRectForTextContainer: method:
UITextView *textView;
CGSize textSize = [textView.layoutManager usedRectForTextContainer:textView.textContainer].size;
To determine how much text actually is shown in the UITextView, you can calculate it by subtracting the text container insets from the frame height.
UITextView *textView;
UIEdgeInsets textInsets = textView.textContainerInset;
CGFloat textViewHeight = textView.frame.size.height - textInsets.top - textInsets.bottom;
Then it becomes easy to scroll:
// if you want scroll animation, use contentOffset
UITextView *textView;
textView.contentOffset = CGPointMake(textView.contentOffset.x, textSize - textViewHeight);
// if you don't want scroll animation
CGRect scrollBounds = textView.bounds;
scrollBounds.origin = CGPointMake(textView.contentOffset.x, textSize - textViewHeight);
textView.bounds = scrollBounds;
Some numbers for reference on what the different sizes represent for an empty UITextView.
textView.frame.size = (width=246, height=50)
textSize = (width=10, height=16.701999999999998)
textView.contentSize = (width=246, height=33)
textView.textContainerInset = (top=8, left=0, bottom=8, right=0)
Extend UIScrollView to add a scrollToBottom method:
extension UIScrollView {
func scrollToBottom(animated:Bool) {
let offset = self.contentSize.height - self.visibleSize.height
if offset > self.contentOffset.y {
self.setContentOffset(CGPoint(x: 0, y: offset), animated: animated)
}
}
}
To scroll to the bottom end, we have to work with the target view maximum height.
import UIKit
extension UIScrollView {
func scrollToBottomOf(targetView: UIView, animated: Bool) {
setContentOffset(CGPoint(x:targetView.frame.minX, y:targetView.frame.maxY), animated: animated)
}
}
//func invocation example
optionScrollView.scrollToBottomOf(targetView: self.optionsStackView, animated: false)
As explained here
https://janeshswift.com/ios/swift/how-to-scroll-to-a-position-programmatically-in-uiscrollview/
We can create a custom UIScrollView extension as
extension UIScrollView {
func scrollToTop(animated: Bool = false) {
setContentOffset(CGPoint(x: contentOffset.x, y: -adjustedContentInset.top), animated: animated)
}
var bottomContentOffsetY: CGFloat {
max(contentSize.height - bounds.height + adjustedContentInset.bottom, -adjustedContentInset.top)
}
func scrollToBottom(animated: Bool = false) {
setContentOffset(CGPoint(x: contentOffset.x, y: bottomContentOffsetY), animated: animated)
}
func scrollToLeading(animated: Bool = false) {
setContentOffset(CGPoint(x: -adjustedContentInset.left, y: contentOffset.y), animated: animated)
}
var trailingContentOffsetX: CGFloat {
max(contentSize.width - bounds.width + adjustedContentInset.right, -adjustedContentInset.left)
}
func scrollToTrailing(animated: Bool = false) {
setContentOffset(CGPoint(x: trailingContentOffsetX, y: contentOffset.y), animated: animated)
}
func scrollViewToVisible(_ view: UIView, animated: Bool = false) {
scrollRectToVisible(convert(view.bounds, from: view), animated: true)
}
var isOnTop: Bool {
contentOffset.y <= -adjustedContentInset.top
}
var isOnBottom: Bool {
contentOffset.y >= bottomContentOffsetY
}
}
Use It as --
DispatchQueue.main.async {
self.itemsScrollView.scrollToBottom()
}
Xamarin.iOS version for UICollectionView of the accepted answer for ease in copying and pasting
var bottomOffset = new CGPoint (0, CollectionView.ContentSize.Height - CollectionView.Frame.Size.Height + CollectionView.ContentInset.Bottom);
CollectionView.SetContentOffset (bottomOffset, false);

Resources