I'm trying to figure out how to scroll all the way to the bottom of a UICollectionView when the screen first loads. I'm able to scroll to the bottom when the status bar is touched, but I'd like to be able to do that automatically when the view loads as well. The below works fine if I want to scroll to the bottom when the status bar is touched.
- (BOOL)scrollViewShouldScrollToTop:(UITableView *)tableView
{
NSLog(#"Detect status bar is touched.");
[self scrollToBottom];
return NO;
}
-(void)scrollToBottom
{//Scrolls to bottom of scroller
CGPoint bottomOffset = CGPointMake(0, collectionViewReload.contentSize.height - collectionViewReload.bounds.size.height);
[collectionViewReload setContentOffset:bottomOffset animated:NO];
}
I've tried calling [self scrollToBottom] in the viewDidLoad. This isn't working. Any ideas on how I can scroll to the bottom when the view loads?
I found that nothing would work in viewWillAppear. I can only get it to work in viewDidLayoutSubviews:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
[self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:endOfModel inSection:0] atScrollPosition:UICollectionViewScrollPositionNone animated:NO];
}
Just to elaborate on my comment.
viewDidLoad is called before elements are visual so certain UI elements cannot be manipulated very well. Things like moving buttons around work but dealing with subviews often does not (like scrolling a CollectionView).
Most of these actions will work best when called in viewWillAppear or viewDidAppear. Here is an except from the Apple docs that points out an important thing to do when overriding either of these methods:
You can override this method to perform additional tasks associated
with presenting the view. If you override this method, you must call
super at some point in your implementation.
The super call is generally called before custom implementations. (so the first line of code inside of the overridden methods).
So had a similar issue and here is another way to come at it without using scrollToItemAtIndexPath
This will scroll to the bottom only if the content is larger than the view frame.
It's probably better to use scrollToItemAtIndexPath but this is just another way to do it.
CGFloat collectionViewContentHeight = myCollectionView.contentSize.height;
CGFloat collectionViewFrameHeightAfterInserts = myCollectionView.frame.size.height - (myCollectionView.contentInset.top + myCollectionView.contentInset.bottom);
if(collectionViewContentHeight > collectionViewFrameHeightAfterInserts) {
[myCollectionView setContentOffset:CGPointMake(0, myCollectionView.contentSize.height - myCollectionView.frame.size.height) animated:NO];
}
Swift 3 example
let sectionNumber = 0
self.collectionView?.scrollToItem(at: //scroll collection view to indexpath
NSIndexPath.init(row:(self.collectionView?.numberOfItems(inSection: sectionNumber))!-1, //get last item of self collectionview (number of items -1)
section: sectionNumber) as IndexPath //scroll to bottom of current section
, at: UICollectionViewScrollPosition.bottom, //right, left, top, bottom, centeredHorizontally, centeredVertically
animated: true)
Get indexpath for last item. Then...
- (void)scrollToItemAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UICollectionViewScrollPosition)scrollPosition animated:(BOOL)animated
For me, i found next solution:
call reloadData in CollectionView, and make dcg on main to scroll.
__weak typeof(self) wSelf = self;
[wSelf.cv reloadData];
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"HeightGCD:%#", #(wSelf.cv.contentSize.height));
[wSelf.cv scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:50 inSection:0] atScrollPosition:UICollectionViewScrollPositionBottom animated:YES];
});
none of these were working so well for me, I ended up with this which will work on any scroll view
extension UIScrollView {
func scrollToBottom(animated: Bool) {
let y = contentSize.height - 1
let rect = CGRect(x: 0, y: y + safeAreaInsets.bottom, width: 1, height: 1)
scrollRectToVisible(rect, animated: animated)
}
}
The issue is likely that even if your collection view is on screen, it might not have the actual contentSize.
If you scroll in viewDidAppear, you will have a contentSize, but your scollectionview will briefly show content before scrolling.
And the problem with viewDidLayoutSubviews is that it is called multiple times, so you then need to add an ugly boolean to limit scrolling.
The best solution i've found is to force layout in view will appear.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// force layout before scrolling to most recent
collectionView.layoutIfNeeded()
// now you can scroll however you want
// e.g. scroll to the right
let offset = collectionView.contentSize.width - collectionView.bounds.size.width
collectionView.setContentOffSet(CGPoint(x: offset, y: 0), animated: animated)
}
Consider if you can use performBatchUpdates like this:
private func reloadAndScrollToItem(at index: Int, animated: Bool) {
collectionView.reloadData()
collectionView.performBatchUpdates({
self.collectionView.scrollToItem(at: IndexPath(item: index, section: 0),
at: .bottom,
animated: animated)
}, completion: nil)
}
If index is the index of the last item in the collection's view data source it'll scroll all the way to the bottom.
Related
When my View (containing a Table View) loads, I want it to scroll to the bottom of the table view. This works great. However, when one of my buttons (sendReply) is tapped, I also want the tableView to scroll to the bottom. For some reason, scrolling to the bottom of the tableView works when the View is initially loaded, however [self bottomScroll:self] doesn't seem to fire when I place it inside of my sendReply action?
.m
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self bottomScroll:self];
}
- (void)bottomScroll:(id)sender {
if (self.messages.count > 0)
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:self.messages.count-1 inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}
- (IBAction)sendReply:(id)sender {
[self bottomScroll:self];
}
This should work. Are you sure messages.count is greater than 0 when you tap the button? You can set a breakpoint at that line and see if this code is executed.
Also you can try -scrollRectToVisible as an alternative.
as phi stated scrollRectToVisible would be the way I would go. Call your tableView's scroll view passing in the rect of your button you want to show. It's been awhile since I've used obj-c but:
-(IBAction)sendReply: (id)sender {
[scrollView scrollRectToVisible: sendReply.frame, animated: YES]
}
? The syntax may be wrong there but it should be close enough to get you where you are going.
In swift:
#IBAction func sendReply(sender: UIButton/AnyObject) -> Void {
let scrollView = tableView.scrollView
scrollView.scrollrectToVisible(_ rect: sender.frame, animated: true)
}
It seems strange based on your description. Need more code to find whats going on. What can be inferred is you want to scroll the last cell to the bottom. So make sure the contentsize of tableview is larger than its frame vertically.
Try this one
if (self.tableView.contentSize.height > self.tableView.frame.size.height){
CGPoint point = CGPointMake(0, self.tableView.contentSize.height - self.tableView.frame.size.height);
[self.tableView setContentOffset:point];
}
I have one CollectionViewController and have 10 cells in it.
But when I run the app it displays the cell at bottom, but I want to display it from top cell at viewDidLoad.
I tried many things but it didn't work.
How to fix it?
in swift 3: after reloadData()
self.collectionView.setContentOffset(CGPoint(x:0,y:0), animated: true)
Its work fine......
collectionView.setContentOffset(.zero, animated: false)
Add this line after reloading the collection view.
[self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionTop animated:NO];
Edit:
Just want to add one more point here is if collectionView datasource has ZERO elements or it is nil then above code will not work and probably crash your app.
Write condition to check that datasource is available or not!
if (self.dataArray.count > 0) {
[self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionTop animated:NO];
}
Where, dataArray is your datasource.
Add this piece of code after reloading the collection view:
swift
yourCollectionView.setContentOffset(CGPoint.zero, animated: true)
objective-c
[yourCollectionView setContentOffset:CGPointZero animated:YES];
Swift 4 Swift 5
Actually, it's work just do
collectionView.setContentOffset(.zero, animated: false)
But, if in case your CollectionView implement .contentInset or outer margin, let's say margin top & bottom is set 16, this code will actually back to the top.
collectionView.setContentOffset(CGPoint(x: 0, y: -16), animated: true)
Please try this one
[self.yourcollectionview scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionTop animated:NO];
Make animated option false. Maybe some view animations are running during your collection view animation process.
collectionView.scrollToItem(at: indexPath, at: .top, animated: false)
To display the data from top cell in collectionView you can use one method
[collectionView setScrollsToTop:YES];
Hope it will Help You.
I am using iOS 8 new self-sizing cells. Visually it works good - each cell gets its right size. However, if I try to scroll to the last row, the table view doesn't seem to know its right size. Is this a bug or is there a fix for that?
Here's how to recreate the problem:
Using this project - TableViewCellWithAutoLayoutiOS8 (referenced from this SO answer), I got the auto-resizing cells as expected.
However, if I am calling the scrollToRowAtIndexPath function, like this:
tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: model.dataArray.count - 1, inSection: 0), atScrollPosition: .Bottom, animated: true)
I do not get to the last row - It only gets me around halfway there.
Even by trying to use a lower level function like this:
tableView.setContentOffset(CGPointMake(0, tableView.contentSize.height - tableView.frame.size.height), animated: true)
The result is not as expected, it won't get to the end. If I click it a lot of times or wait a few moments, eventually it will get to the right place. It seems the tableView.contentSize.height is not set correctly, so the iOS "doesn't know" where that last cell is.
Would appreciate any help.
Thanks
Update: Jun 24, 2015
Apple has addressed most of these bugs as of the iOS 9.0 SDK. All of the issues are fixed as of iOS 9 beta 2, including scrolling to the top & bottom of the table view without animation, and calling reloadData while scrolled in the middle of the table view.
Here are the remaining issues that have not been fixed yet:
When using a large estimated row height, scrolling to the last row with animation causes the table view cells to disappear.
When using a small estimated row height, scrolling to the last row with animation causes the table view to finish scrolling too early, leaving some cells below the visible area (and the last row still offscreen).
A new bug report (rdar://21539211) has been filed for these issues relating to scrolling with animation.
Original Answer
This is an Apple bug with the table view row height estimation, and it has existed since this functionality first was introduced in iOS 7. I have worked directly with Apple UIKit engineers and developer evangelists on this issue -- they have acknowledged that it is a bug, but do not have any reliable workaround (short of disabling row height estimation), and did not seem particularly interested in fixing it.
Note that the bug manifests itself in other ways, such as disappearing table view cells when you call reloadData while scrolled partially or fully down (e.g. contentOffset.y is significantly greater than 0).
Clearly, with iOS 8 self sizing cells, row height estimation is critically important, so Apple really needs to address this ASAP.
I filed this issue back on Oct 21 2013 as Radar #15283329. Please do file duplicate bug reports so that Apple prioritizes a fix.
You can attach this simple sample project to demonstrate the issue. It is based directly on Apple's own sample code.
This has been a very annoying bug, but I think I found a permanent solution, though I cannot fully explain why.
Call the function after a tiny (unnoticed) delay:
let delay = 0.1 * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue(), {
tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: model.dataArray.count - 1, inSection: 0), atScrollPosition: .Bottom, animated: true)
})
Do tell me if this works for you as well.
It is definitely a bug from Apple. I also have this problem. I solved this problem by calling "scrollToRowAtIndexPath" method twice example code is:
if array.count > 0 {
let indexPath: NSIndexPath = NSIndexPath(forRow: array.count - 1, inSection: 0)
self.tblView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Bottom, animated: true)
let delay = 0.1 * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue(), {
self.tblView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Bottom, animated: true)
})
}
I found a temporary workaround that might be helpful until Apple decides to fixes the many bugs that have been plaguing us.
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *text = [self findTextForIndexPath:indexPath];
UIFont *font = [UIFont fontWithName:#"HelveticaNeue" size:13];
CGRect estimatedHeight = [text boundingRectWithSize:CGSizeMake(215, MAXFLOAT)
options:NSStringDrawingUsesLineFragmentOrigin
attributes:#{NSFontAttributeName: font}
context:nil];
return TOP_PADDING + CGRectGetHeight(estimatedHeight) + BOTTOM_PADDING;
}
This is not perfect, but it did the job for me. Now I can call:
- (void)scrollToLastestSeenMessageAnimated:(BOOL)animated
{
NSInteger count = [self tableView:self.tableView numberOfRowsInSection:0];
if (count > 0) {
NSInteger lastPos = MAX(0, count-1);
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForItem:lastPos inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:animated];
}
}
On viewDidLayoutSubviews and it finds the correct place on the bottom (or a very close estimated position).
I hope that helps.
For my case, I found a temporary workaround by not suggesting an estimated cell height to the program. I did this by commenting out the following method in my code:
- (CGFloat) tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
However, please take note that doing so may affect the user experience when the user scrolls, if your cells varies a lot compared to each other. For my case, no noticeable difference so far.
Hope it helps!
I have this problem in Swift 5 iOS 13 yet, this solved my problem
DispatchQueue.main.async { [weak self] in
self?.tableView.reloadData()
self?.tableView.scrollToRow(at: indexPath, at: .middle, animated: false)
}
My solution was to use the size of the storyboard as the estimate.
So instead of this:
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
I did something like this:
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
MyMessageType messageType = [self messageTypeForRowAtIndexPath:indexPath];
switch (messageType) {
case MyMessageTypeText:
return 45;
break;
case MyMessageTypeMaybeWithSomeMediaOrSomethingBiggerThanJustText:
return 96;
break;
default:
break;
}
}
I'm writing a chat table view so it is likely that many of my cells, specifically that text type will be larger than what is in IB, especially if the chat message is very long. This seems to be a pretty good...well...estimate and scrolling to the bottom gets pretty close. It seems to be slightly worse as the scrolling gets longer, but that is to be expected I suppose
Just call tableview reloadData after viewDidAppear can solve the problem
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self.tableView reloadData];
}
Although as smileyborg's answer it is bug in iOS 8.x, it should be fixed in all platforms which you supports...
To workaround on pre-iOS9, below code do the trick without any dispatch_async or dispatch_after.
Tested on iOS 8.4 simulator.
UPDATE: Calling (only) layoutIfNeeded does not work when view controller become visible by UIPageViewController being scrolled. So use layoutSubviews (or maybe setNeedsLayout + layoutIfNeeded) instead.
// For iOS 8 bug workaround.
// See https://stackoverflow.com/a/33515872/1474113
- (void)scrollToBottomForPreiOS9
{
CGFloat originalY, scrolledY;
do {
// Lay out visible cells immediately for current contentOffset.
// NOTE: layoutIfNeeded does not work when hosting UIPageViewController is dragged.
[self.tableView layoutSubviews];
originalY = self.tableView.contentOffset.y;
[self scrollToBottom]; // Call -scrollToRowAtIndexPath as usual.
scrolledY = self.tableView.contentOffset.y;
} while (scrolledY > originalY);
}
I had the same problem when creating a chat tableView with different height of cells. I call the code below in viewDidAppear() lifecycle method:
// First figure out how many sections there are
let lastSectionIndex = self.tableView.numberOfSections - 1
// Then grab the number of rows in the last section
let lastRowIndex = self.tableView.numberOfRowsInSection(lastSectionIndex) - 1
// Now just construct the index path
let pathToLastRow = NSIndexPath(forRow: lastRowIndex, inSection: lastSectionIndex)
// Make the last row visible
self.tableView.scrollToRowAtIndexPath(pathToLastRow, atScrollPosition: UITableViewScrollPosition.None, animated: true)
Please let me know if that worked for you too.
From the storyboard window click in a blank area to deselect all views then click the view that has the table view in it and then click the Resolve Auto Layout Issue icon and select Reset to Suggested Constraints
Use this simple code to scroll bottom
var rows:NSInteger=self.tableName.numberOfRowsInSection(0)
if(rows > 0)
{
let indexPath = NSIndexPath(forRow: rows-1, inSection: 0)
tableName.scrollToRowAtIndexPath(indexPath , atScrollPosition: UITableViewScrollPosition.Bottom, animated: true)
}
}
I have a UITableView and am currently working on adding a custom search bar at the top consisting of a UIView with a UITextField inside of it. As is standard for iOS apps, the search bar should only be visible when the table view is scrolled to the top — when scrolling down it should disappear off the screen along with the other cells.
However, I cannot figure out a way to achieve this effect. If I place the search bar at the top of the table view, it will overlay the cell beneath it. If I place it 50 pixels above the table view, it is not possible for the user to select it since it will automatically disappear when the user releases their finger from the screen.
Can someone please enlighten me as to how to achieve this effect?
Usually you just use UISearchBar as a tableViewHeader for your table view. If you want it hidden when the user enters the screen (like it's done in most native apps) you can just set contentOffset for the tableView in viewWillAppear.
And I am pretty sure that's in fact how they do it. And if you think about it it's what tableHeaderView is meant for.
Try something like this:
- (void)viewDidLoad
{
[super viewDidLoad];
UISearchBar *searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0., 0., 320., 44.)];
self.tableView.tableHeaderView = searchBar;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
CGPoint contentOffset = self.tableView.contentOffset;
contentOffset.y += CGRectGetHeight(self.tableView.tableHeaderView.frame);
self.tableView.contentOffset = contentOffset;
}
Note that in iOS 7 you should not just set the contentOffset of your tableView to CGPointMake(0., CGRectGetHeight(self.tableView.tableHeaderView.frame)) if your viewController has automaticallyAdjustsScrollViewInsets set to YES, since it probably will not be CGPointZero in viewWillAppear:
#dariaa's answer updated for Swift 3:
override func viewDidLoad() {
super.viewDidLoad()
let searchBar = UISearchBar(frame: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: 44))
self.tableView.tableHeaderView = searchBar
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
var contentOffset: CGPoint = self.tableView.contentOffset
contentOffset.y += (self.tableView.tableHeaderView?.frame)!.height
self.tableView.contentOffset = contentOffset
}
Though you'll probably need to set the searchBar as a property if you want to use it.
If you only want your search to be visible when the UITableView is scrolled all the way to the top, make a UITableViewCell subclass that houses your UISearchBar. Then, in tableView:cellForRowAtIndexPath, check if the indexPath is (0,0). This is the table view telling you it is creating the cell at the very top, so then just create your search bar cell instead of your default cell.
Code would look something like this:
- (void)viewDidLoad
{
[self.tableView registerClass:[SearchBarCell class] forCellReuseIdentifier:#"SearchBarCell"];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row == 0 && indexPath.section == 0) {
//this is the cell that displays the UISearchBar
SearchBarCell *cell = [tableView dequeueReusableCellWithIdentifier:#"SearchBarCell"];
return cell;
}
else {
//create your usual table view cell normally
}
}
There may be a cleaner way to determine the row and section of the indexPath, but I'm writing this code off the top of my head and don't recall a better way.
In iOS 7, given a UICollectionView, how do you start it at the bottom? Think about the iOS Messages app, where when the view becomes visible it always starts at the bottom (most recent message).
#awolf
Your solution is good!
But do not work well with autolayout.
You should call [self.view layoutIfNeeded] first!
Full solution is:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// ---- autolayout ----
[self.view layoutIfNeeded];
CGSize contentSize = [self.collectionView.collectionViewLayout collectionViewContentSize];
if (contentSize.height > self.collectionView.bounds.size.height) {
CGPoint targetContentOffset = CGPointMake(0.0f, contentSize.height - self.collectionView.bounds.size.height);
[self.collectionView setContentOffset:targetContentOffset];
}
}
The problem is that if you try to set the contentOffset of your collection view in viewWillAppear, the collection view hasn't rendered its items yet. Therefore self.collectionView.contentSize is still {0,0}. The solution is to ask the collection view's layout for the content size.
Additionally, you'll want to make sure that you only set the contentOffset when the contentSize is taller than the bounds of your collection view.
A full solution looks like:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
CGSize contentSize = [self.collectionView.collectionViewLayout collectionViewContentSize];
if (contentSize.height > self.collectionView.bounds.size.height) {
CGPoint targetContentOffset = CGPointMake(0.0f, contentSize.height - self.collectionView.bounds.size.height);
[self.collectionView setContentOffset:targetContentOffset];
}
}
This works for me and i think it is a modern way.
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.collectionView!.scrollToItemAtIndexPath(indexForTheLast, atScrollPosition: UICollectionViewScrollPosition.Bottom, animated: false)
}
yourCollectionView.contentOffset = CGPointMake(0, yourCollectionView.contentSize.height - yourCollectionView.bounds.size.height);
But remember to do this only when your contentSize.height > bounds.size.height.
Assuming that you know how many items are in your collection view you can use
scrollToItemAtIndexPath:atScrollPosition:animated:
Apple Docs
it works perfectly for me (autolayout)
Calculate ScrollView's Content Size using collectionViewFlowLayout and cellSize
collectionView.contentSize = calculatedContentSize
collectionView.scrollToItemAtIndexPath(whichYouWantToScrollIndexPath, atScrollPosition: ...)
added in scrollToItemAtIndexPath:atScrollPosition:animated: in viewWillLayoutSubviews so that the collectionView will be loaded instantly at the bottom