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

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
}

Related

How to create a centered UICollectionView like in Spotify's Player

I am have a lot of difficulty trying to create a UICollectionView like in Spotify's Player that acts like this:
The problem for me is two fold.
1) How do I center the cells so that you can see the middle cell as well as the one of the left and right.
If I create cells that are square and add spacing between each cell, the cells are correctly displayed but are not centered.
2) With pagingEnabled = YES, the collectionview correctly swipes from one page to another. However, without the cells being centered, it simply moves the collection view over a page which is the width of the screen. So the question is how do you make the pages move so you get the effect above.
3) how do you animate the size of the cells as they move
I don't want to worry about this too much. If I can get that to work it would be great, but the harder problems are 1 and 2.
The code I have currently is a simple UICollectionView with normal delegate setup and custom UICollectionview cells that are squares. Maybe I neeed to subclass UICollectionViewFlowLayout? Or maybe I need to turn pagingEnabled to NO and then use custom swipe events? Would love any help!
In order to create an horizontal carousel layout, you'll have to subclass UICollectionViewFlowLayout then override targetContentOffset(forProposedContentOffset:withScrollingVelocity:), layoutAttributesForElements(in:) and shouldInvalidateLayout(forBoundsChange:).
The following Swift 5 / iOS 12.2 complete code shows how to implement them.
CollectionViewController.swift
import UIKit
class CollectionViewController: UICollectionViewController {
let collectionDataSource = CollectionDataSource()
let flowLayout = ZoomAndSnapFlowLayout()
override func viewDidLoad() {
super.viewDidLoad()
title = "Zoomed & snapped cells"
guard let collectionView = collectionView else { fatalError() }
//collectionView.decelerationRate = .fast // uncomment if necessary
collectionView.dataSource = collectionDataSource
collectionView.collectionViewLayout = flowLayout
collectionView.contentInsetAdjustmentBehavior = .always
collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
}
}
ZoomAndSnapFlowLayout.swift
import UIKit
class ZoomAndSnapFlowLayout: UICollectionViewFlowLayout {
let activeDistance: CGFloat = 200
let zoomFactor: CGFloat = 0.3
override init() {
super.init()
scrollDirection = .horizontal
minimumLineSpacing = 40
itemSize = CGSize(width: 150, height: 150)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepare() {
guard let collectionView = collectionView else { fatalError() }
let verticalInsets = (collectionView.frame.height - collectionView.adjustedContentInset.top - collectionView.adjustedContentInset.bottom - itemSize.height) / 2
let horizontalInsets = (collectionView.frame.width - collectionView.adjustedContentInset.right - collectionView.adjustedContentInset.left - itemSize.width) / 2
sectionInset = UIEdgeInsets(top: verticalInsets, left: horizontalInsets, bottom: verticalInsets, right: horizontalInsets)
super.prepare()
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView else { return nil }
let rectAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.frame.size)
// Make the cells be zoomed when they reach the center of the screen
for attributes in rectAttributes where attributes.frame.intersects(visibleRect) {
let distance = visibleRect.midX - attributes.center.x
let normalizedDistance = distance / activeDistance
if distance.magnitude < activeDistance {
let zoom = 1 + zoomFactor * (1 - normalizedDistance.magnitude)
attributes.transform3D = CATransform3DMakeScale(zoom, zoom, 1)
attributes.zIndex = Int(zoom.rounded())
}
}
return rectAttributes
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else { return .zero }
// Add some snapping behaviour so that the zoomed cell is always centered
let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView.frame.width, height: collectionView.frame.height)
guard let rectAttributes = super.layoutAttributesForElements(in: targetRect) else { return .zero }
var offsetAdjustment = CGFloat.greatestFiniteMagnitude
let horizontalCenter = proposedContentOffset.x + collectionView.frame.width / 2
for layoutAttributes in rectAttributes {
let itemHorizontalCenter = layoutAttributes.center.x
if (itemHorizontalCenter - horizontalCenter).magnitude < offsetAdjustment.magnitude {
offsetAdjustment = itemHorizontalCenter - horizontalCenter
}
}
return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
// Invalidate layout so that every cell get a chance to be zoomed when it reaches the center of the screen
return true
}
override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
let context = super.invalidationContext(forBoundsChange: newBounds) as! UICollectionViewFlowLayoutInvalidationContext
context.invalidateFlowLayoutDelegateMetrics = newBounds.size != collectionView?.bounds.size
return context
}
}
CollectionDataSource.swift
import UIKit
class CollectionDataSource: NSObject, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 9
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
return cell
}
}
CollectionViewCell.swift
import UIKit
class CollectionViewCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .green
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Expected result:
Source:
WWDC 2012 session 219 - "Advanced Collection Views and Building Custom Layouts"
Well, I made UICollectionview moving just like this, yesterday.
I can share my code with you :)
Here's my storyboard
make sure uncheck 'Paging Enabled'
Here's my code.
#interface FavoriteViewController () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
{
NSMutableArray * mList;
CGSize cellSize;
}
#property (weak, nonatomic) IBOutlet UICollectionView *cv;
#end
#implementation FavoriteViewController
- (void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// to get a size.
[self.view setNeedsLayout];
[self.view layoutIfNeeded];
CGRect screenFrame = [[UIScreen mainScreen] bounds];
CGFloat width = screenFrame.size.width*self.cv.frame.size.height/screenFrame.size.height;
cellSize = CGSizeMake(width, self.cv.frame.size.height);
// if cell's height is exactly same with collection view's height, you get an warning message.
cellSize.height -= 1;
[self.cv reloadData];
// setAlpha is for hiding looking-weird at first load
[self.cv setAlpha:0];
}
- (void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self scrollViewDidScroll:self.cv];
[self.cv setAlpha:1];
}
#pragma mark - scrollview delegate
- (void) scrollViewDidScroll:(UIScrollView *)scrollView
{
if(mList.count > 0)
{
const CGFloat centerX = self.cv.center.x;
for(UICollectionViewCell * cell in [self.cv visibleCells])
{
CGPoint pos = [cell convertPoint:CGPointZero toView:self.view];
pos.x += cellSize.width/2.0f;
CGFloat distance = fabs(centerX - pos.x);
// If you want to make side-cell's scale bigger or smaller,
// change the value of '0.1f'
CGFloat scale = 1.0f - (distance/centerX)*0.1f;
[cell setTransform:CGAffineTransformMakeScale(scale, scale)];
}
}
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{ // for custom paging
CGFloat movingX = velocity.x * scrollView.frame.size.width;
CGFloat newOffsetX = scrollView.contentOffset.x + movingX;
if(newOffsetX < 0)
{
newOffsetX = 0;
}
else if(newOffsetX > cellSize.width * (mList.count-1))
{
newOffsetX = cellSize.width * (mList.count-1);
}
else
{
NSUInteger newPage = newOffsetX/cellSize.width + ((int)newOffsetX%(int)cellSize.width > cellSize.width/2.0f ? 1 : 0);
newOffsetX = newPage*cellSize.width;
}
targetContentOffset->x = newOffsetX;
}
#pragma mark - collectionview delegate
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return mList.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"list" forIndexPath:indexPath];
NSDictionary * dic = mList[indexPath.row];
UIImageView * iv = (UIImageView *)[cell.contentView viewWithTag:1];
UIImage * img = [UIImage imageWithData:[dic objectForKey:kKeyImg]];
[iv setImage:img];
return cell;
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return cellSize;
}
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{
CGFloat gap = (self.cv.frame.size.width - cellSize.width)/2.0f;
return UIEdgeInsetsMake(0, gap, 0, gap);
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section
{
return 0;
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section
{
return 0;
}
Key code of make cell centered is
scrollViewWillEndDragging
insetForSectionAtIndex
Key code of animate the size is
scrollviewDidScroll
I wish this helps you
P.S.
If you want to change alpha just like the image that you uploaded, add [cell setalpha] in scrollViewDidScroll
As you have said in the comment you want that in the Objective-c code, there is a very famous library called iCarousel which can be helpful in completing your requirement.Link: https://github.com/nicklockwood/iCarousel
You may use 'Rotary' or 'Linear' or some other style with little or no modification to implement the custom view
To implement it you have implement only some delegate methods of it and it's working for ex:
//specify the type you want to use in viewDidLoad
_carousel.type = iCarouselTypeRotary;
//Set the following delegate methods
- (NSInteger)numberOfItemsInCarousel:(iCarousel *)carousel
{
//return the total number of items in the carousel
return [_items count];
}
- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSInteger)index reusingView:(UIView *)view
{
UILabel *label = nil;
//create new view if no view is available for recycling
if (view == nil)
{
//don't do anything specific to the index within
//this `if (view == nil) {...}` statement because the view will be
//recycled and used with other index values later
view = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 200.0f, 200.0f)];
((UIImageView *)view).image = [UIImage imageNamed:#"page.png"];
view.contentMode = UIViewContentModeCenter;
label = [[UILabel alloc] initWithFrame:view.bounds];
label.backgroundColor = [UIColor clearColor];
label.textAlignment = NSTextAlignmentCenter;
label.font = [label.font fontWithSize:50];
label.tag = 1;
[view addSubview:label];
}
else
{
//get a reference to the label in the recycled view
label = (UILabel *)[view viewWithTag:1];
}
//set item label
label.text = [_items[index] stringValue];
return view;
}
- (CGFloat)carousel:(iCarousel *)carousel valueForOption:(iCarouselOption)option withDefault:(CGFloat)value
{
if (option == iCarouselOptionSpacing)
{
return value * 1.1;
}
return value;
}
You can check the full working demo from 'Examples/Basic iOS Example' which is included with the Github repository link
As it is old and popular you can find some related tutorials for it and it will also be much stable than the custom code implementation
I wanted similar behavior a little while back, and with the help of #Mike_M I was able to figure it out. Although there are many, many way to do this, this particular implementation is to create a custom UICollectionViewLayout.
Code below(gist can be found here: https://gist.github.com/mmick66/9812223)
Now it's important to set the following: *yourCollectionView*.decelerationRate = UIScrollViewDecelerationRateFast, this prevents cells being skipped by a quick swipe.
That should cover part 1 and 2. Now, for part 3 you could incorporate that in the custom collectionView by constantly invalidating and updating, but it's a bit of a hassle if you ask me. So another approach would be to to set a CGAffineTransformMakeScale( , ) in the UIScrollViewDidScroll where you dynamically update the cell's size based on it's distance from the center of the screen.
You can get the indexPaths of the visible cells of the collectionView using [*youCollectionView indexPathsForVisibleItems] and then getting the cells for these indexPaths. For every cell, calculate the distance of its center to the center of yourCollectionView
The center of the collectionView can be found using this nifty method: CGPoint point = [self.view convertPoint:*yourCollectionView*.center toView:*yourCollectionView];
Now set up a rule, that if the cell's center is further than x away, the size of the cell is for example the 'normal size', call it 1. and the closer it gets to the center, the closer it gets to twice the normal size 2.
then you can use the following if/else idea:
if (distance > x) {
cell.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
} else if (distance <= x) {
float scale = MIN(distance/x) * 2.0f;
cell.transform = CGAffineTransformMakeScale(scale, scale);
}
What happens is that the cell's size will exactly follow your touch. Let me know if you have any more questions as I'm writing most of this out of the top of my head).
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)offset
withScrollingVelocity:(CGPoint)velocity {
CGRect cvBounds = self.collectionView.bounds;
CGFloat halfWidth = cvBounds.size.width * 0.5f;
CGFloat proposedContentOffsetCenterX = offset.x + halfWidth;
NSArray* attributesArray = [self layoutAttributesForElementsInRect:cvBounds];
UICollectionViewLayoutAttributes* candidateAttributes;
for (UICollectionViewLayoutAttributes* attributes in attributesArray) {
// == Skip comparison with non-cell items (headers and footers) == //
if (attributes.representedElementCategory !=
UICollectionElementCategoryCell) {
continue;
}
// == First time in the loop == //
if(!candidateAttributes) {
candidateAttributes = attributes;
continue;
}
if (fabsf(attributes.center.x - proposedContentOffsetCenterX) <
fabsf(candidateAttributes.center.x - proposedContentOffsetCenterX)) {
candidateAttributes = attributes;
}
}
return CGPointMake(candidateAttributes.center.x - halfWidth, offset.y);
}
pagingEnabled should not be enabled as it needs each cell to be the width of you view which will not work for you since you need to see the edges of other cells. For your points 1 and 2. I think you'll find what you need here from one of my late answers to another question.
The animation of the cell sizes can be achieved by subclassing UIcollectionviewFlowLayout and overriding layoutAttributesForItemAtIndexPath: Within that modify the layout attributes provided by first calling super and then modify the layout attributes size based on the position as it relates to the window centre.
Hopefully this helps.
If you want to have uniform spacing between cells you can replace the following method in ZoomAndSnapFlowLayout from Imanou Petit's solution:
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView else { return nil }
let rectAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.frame.size)
let visibleAttributes = rectAttributes.filter { $0.frame.intersects(visibleRect) }
// Keep the spacing between cells the same.
// Each cell shifts the next cell by half of it's enlarged size.
// Calculated separately for each direction.
func adjustXPosition(_ toProcess: [UICollectionViewLayoutAttributes], direction: CGFloat, zoom: Bool = false) {
var dx: CGFloat = 0
for attributes in toProcess {
let distance = visibleRect.midX - attributes.center.x
attributes.frame.origin.x += dx
if distance.magnitude < activeDistance {
let normalizedDistance = distance / activeDistance
let zoomAddition = zoomFactor * (1 - normalizedDistance.magnitude)
let widthAddition = attributes.frame.width * zoomAddition / 2
dx = dx + widthAddition * direction
if zoom {
let scale = 1 + zoomAddition
attributes.transform3D = CATransform3DMakeScale(scale, scale, 1)
}
}
}
}
// Adjust the x position first from left to right.
// Then adjust the x position from right to left.
// Lastly zoom the cells when they reach the center of the screen (zoom: true).
adjustXPosition(visibleAttributes, direction: +1)
adjustXPosition(visibleAttributes.reversed(), direction: -1, zoom: true)
return rectAttributes
}

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 Can I Animate the Size of A UICollectionViewCell on Scroll so the middle one is largest?

I have a UICollectionView that shows several rows with one, full-width column (looks like a UITableView)
What I'd like to achieve is something similar to this:
... where the middle cell has a much greater height. As the user scrolls up and down, the cells before and after the middle cell animate back to the default height for the given cell.
Can somebody outline how I should approach this problem?
I use this Swift 3 code in my horizontal UICollectionView.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let centerX = scrollView.contentOffset.x + scrollView.frame.size.width/2
for cell in mainCollectionView.visibleCells {
var offsetX = centerX - cell.center.x
if offsetX < 0 {
offsetX *= -1
}
cell.transform = CGAffineTransform(scaleX: 1, y: 1)
if offsetX > 50 {
let offsetPercentage = (offsetX - 50) / view.bounds.width
var scaleX = 1-offsetPercentage
if scaleX < 0.8 {
scaleX = 0.8
}
cell.transform = CGAffineTransform(scaleX: scaleX, y: scaleX)
}
}
}
You need to create a custom subclass of UICollectionViewLayout.
First of all, override - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds to return yes, that way, you can change the layout attributes of your cells as your collection is being scrolled.
After that, your key methods to override are:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
and
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
I suggest reading an article about custom collection view layouts. It can be pretty heavy subject matter.
since the UICollectionView is the subclass of UIScrollView, you can solved this problem by treating your collectionView as a scrollView.
set the UIScrollViewDelegate and implemented scrollViewDidScroll, and then do something like this:
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
for (int i = 0; i < [scrollView.subviews count]; i++) {
UIView *cell = [scrollView.subviews objectAtIndex:i];
float position = cell.center.y - scrollView.contentOffset.y;
float offset = 1.5 - (fabs(scrollView.center.y - position) * 1.0) / scrollView.center.y;
if (offset<1.0)
{
offset=1.0;
}
cell.transform = CGAffineTransformIdentity;
cell.transform = CGAffineTransformScale(cell.transform, offset, offset);
}
}
hope this will solve your problem.

how to get indexPath for cell which is located in the center of UICollectionView

How can i find indexPath for cell in the middle of UICollectionView?
I have horizontal scrolling and only one big cell is visible (partially two other cells on the sides are visible as well).
I need to delete cell located in the center (means - current cell) without touching it.
Only pressing "Trash" button and confirm Delete. But now it delete only first cells.
- (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex {
if (buttonIndex == actionSheet.destructiveButtonIndex) {
initialPinchPoint = ????????
self.tappedCellPath = [self.collectionView indexPathForItemAtPoint:initialPinchPoint];
Technique *technique = [arrayOfTechnique objectAtIndex:self.tappedCellPath.row];
[self deleteData:[NSString stringWithFormat:#"DELETE FROM TECHNIQUES WHERE TECHNIQUENAME IS '%s'",[technique.techniquename UTF8String]]];
[arrayOfTechnique removeObjectAtIndex:self.tappedCellPath.row];
//[arrayOfTechnique removeObjectAtIndex:[custom_layout ]];
[self removeImage:technique.techniquename];
[self.collectionView performBatchUpdates:^{
[self.collectionView deleteItemsAtIndexPaths:[NSArrayarrayWithObject:self.tappedCellPath]];
} completion:nil];
[self checkArrayCount];
}
}
Like you did yourself, indexPathForItemAtPoint is a good way of finding the index path of the element you're interested in. If your question is: how do i know the coordinates of this point? Then you should try with (using the same name you gave it in your code snippet):
initialPinchPoint = CGPointMake(self.collectionView.center.x + self.collectionView.contentOffset.x,
self.collectionView.center.y + self.collectionView.contentOffset.y);
Here's what I did in Swift 3
private func findCenterIndex() {
let center = self.view.convert(self.collectionView.center, to: self.collectionView)
let index = collectionView!.indexPathForItem(at: center)
print(index ?? "index not found")
}
This would be better code because it's cleaner and easier to read, all the content offset calculation is superfluous:
NSIndexPath *centerCellIndexPath =
[self.collectionView indexPathForItemAtPoint:
[self.view convertPoint:[self.view center] toView:self.collectionView]];
This would also be the correct representation of what you're actually trying to do:
1. Taking the center point of your viewcontroller's view - aka visual center point
2. convert it to the coordinate space of the view you're interested in - which is the collection view
3. Get the indexpath that exist at the location given.
Swift:
extension UICollectionView {
var centerPoint : CGPoint {
get {
return CGPoint(x: self.center.x + self.contentOffset.x, y: self.center.y + self.contentOffset.y);
}
}
var centerCellIndexPath: IndexPath? {
if let centerIndexPath: IndexPath = self.indexPathForItemAtPoint(self.centerPoint) {
return centerIndexPath
}
return nil
}
}
Usage :
if let centerCellIndexPath: IndexPath = collectionView.centerCellIndexPath {
print(centerCellIndexPath)
}
Swift 3:
extension UICollectionView {
var centerPoint : CGPoint {
get {
return CGPoint(x: self.center.x + self.contentOffset.x, y: self.center.y + self.contentOffset.y);
}
}
var centerCellIndexPath: IndexPath? {
if let centerIndexPath = self.indexPathForItem(at: self.centerPoint) {
return centerIndexPath
}
return nil
}
}
Thank you micantox!
I had multiple visible cells of UICollectionView and needed to position the cell at the centre of the collection view. Something similar to Adobe Content Viewer. If someone is struggling with similar scenario:
#pragma mark - UIScrollViewDelegate methods
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
CGPoint centerPoint = CGPointMake(self.pageContentCollectionView.center.x + self.pageContentCollectionView.contentOffset.x,
self.pageContentCollectionView.center.y + self.pageContentCollectionView.contentOffset.y);
NSIndexPath *centerCellIndexPath = [self.pageContentCollectionView indexPathForItemAtPoint:centerPoint];
[self.pageContentCollectionView scrollToItemAtIndexPath:centerCellIndexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
}
I made like for horizontal UICollectionView I use Swift 2.x.
private func findCenterIndex() {
let collectionOrigin = collectionView!.bounds.origin
let collectionWidth = collectionView!.bounds.width
var centerPoint: CGPoint!
var newX: CGFloat!
if collectionOrigin.x > 0 {
newX = collectionOrigin.x + collectionWidth / 2
centerPoint = CGPoint(x: newX, y: collectionOrigin.y)
} else {
newX = collectionWidth / 2
centerPoint = CGPoint(x: newX, y: collectionOrigin.y)
}
let index = collectionView!.indexPathForItemAtPoint(centerPoint)
print(index)
}
override func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
findCenterIndex()
}
UICollectionView has a method - (NSIndexPath *)indexPathForItemAtPoint:(CGPoint)point.
This method return the index path of the item at the specified point. You could calculate the point that represents the center of the UICollectionView and then use that CGPoint and this method to retrieve the index path of the item you want to delete.
For some of you that might be experiencing some troubles on getting the center, please look at this potential solution:
func centerCell()->UICollectionViewCell? {
// Asuming your scrolling is horizontal
let viewHorizontalCenter = self.view.bounds.width / 2
let center = CGPoint(x: viewHorizontalCenter, y: self.collectionView.center.y)
let convertedPoint = self.view.convert(center, to: self.unitsCollectionView)
let center = CGPoint(x: self.view.bounds.width / 2, y: self.unitsCollectionView.center.y)
let convertedPoint = self.view.convert(center, to: self.unitsCollectionView)
for cell in unitsCollectionView.visibleCells {
if cell.frame.contains(convertedPoint) {
print("Hello")
return cell
}
}
return nil
}
Try this protocol...
protocol CollectionVisibleMidCell {}
extension CollectionVisibleMidCell where Self: UICollectionView {
func getMidVisibleIndexPath() -> IndexPath? {
var visibleRect = CGRect()
visibleRect.origin = self.contentOffset
visibleRect.size = self.bounds.size
let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
guard let indexPath = self.indexPathForItem(at: visiblePoint) else { return nil }
return indexPath
}
}
extension UICollectionView: CollectionVisibleMidCell {}
Swift 4
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let indexPath = collectionView.indexPathForItem(at: collectionView.bounds.center)
}
Based on #micantox answer. Updated code for Swift 5
let initialPinchPoint = CGPoint(x: collectionView.center.x + collectionView.contentOffset.x,
y: collectionView.center.y + collectionView.contentOffset.y)

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