Start UICollectionView at bottom - ios

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

Related

Dynamically resize UICollectionView's supplementary view (containing multiline UILabel's)

Inside a UICollectionView's supplementary view (header), I have a multiline label that I want to truncate to 3 lines.
When the user taps anywhere on the header (supplementary) view, I want to switch the UILabel to 0 lines so all text displays, and grow the collectionView's supplementary view's height accordingly (preferably animated). Here's what happens after you tap the header:
Here's my code so far:
// MyHeaderReusableView.m
// my gesture recognizer's action
- (IBAction)onHeaderTap:(UITapGestureRecognizer *)sender
{
self.listIntro.numberOfLines = 0;
// force -layoutSubviews to run again
[self setNeedsLayout];
[self layoutIfNeeded];
}
- (void)layoutSubviews
{
[super layoutSubviews];
self.listTitle.preferredMaxLayoutWidth = self.listTitle.frame.size.width;
self.listIntro.preferredMaxLayoutWidth = self.listIntro.frame.size.width;
[self layoutIfNeeded];
CGFloat height = [self systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
self.frame = ({
CGRect headerFrame = self.frame;
headerFrame.size.height = height;
headerFrame;
});
NSLog(#"height: %#", #(height));
}
When I log height at the end of layoutSubviews, its value is 149 while the label is truncated and numberOfLines is set to 3. After tapping the headerView, setting numberOfLines to 0, and forcing a layout pass, height then gets recorded as 163.5. Great!
The only problem is that the entire headerView doesn't grow, and the cells don't get pushed down.
How can I dynamically change the height of my collectionView's supplementary view (preferably animated)?
I'm aware of UICollectionViewFlowLayout's headerReferenceSize and collectionView:layout:referenceSizeForHeaderInSection: but not quite sure how I'd use them in this situation.
I got something working, but I'll admit, it feels kludgy. I feel like this could be accomplished with the standard CollectionView (and associated elements) API + hooking into standard layout/display invalidation, but I just couldn't get it working.
The only thing that would resize my headerView was setting my collection view's flow layout's headerReferenceSize. Unfortunately, I can't access my collection view or it's flow layout from my instance of UICollectionReusableView, so I had to create a delegate method to pass the correct height back.
Here's what I have now:
// in MyHeaderReusableView.m
//
// my UITapGestureRecognizer's action
- (IBAction)onHeaderTap:(UITapGestureRecognizer *)sender
{
self.listIntro.numberOfLines = 0;
}
- (void)layoutSubviews
{
[super layoutSubviews];
self.listTitle.preferredMaxLayoutWidth = self.listTitle.frame.size.width;
self.listIntro.preferredMaxLayoutWidth = self.listIntro.frame.size.width;
CGFloat height = [self systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
self.frame = ({
CGRect headerFrame = self.frame;
headerFrame.size.height = height;
headerFrame;
});
if (self.resizeDelegate) {
[self.resizeDelegate wanderlistDetailHeaderDidResize:self.frame.size];
}
}
// in my viewController subclass which owns the UICollectionView:
- (void)wanderlistDetailHeaderDidResize:(CGSize)newSize
{
UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout;
// this is the key line
flowLayout.headerReferenceSize = newSize;
// this doesn't look beautiful but it's the best i can do for now. I would love for just the bottom of the frame to animate down, but instead, all the contents in the header (the top labels) have a crossfade effect applied.
[UIView animateWithDuration:0.3 animations:^{
[self.collectionView layoutIfNeeded];
}];
}
Like I said, not the solution I was looking for, but a working solution nonetheless.
I ran into the same issue than you, so I was just wondering: did you ever get a solution without the crossfade effect that you mention in the code sample?. My approach was pretty much the same, so I get the same problem. One additional comment though: I managed to implement the solution without the need for delegation: What I did was from "MyHeaderReusableView.m" You can reference the UICollectionView (and therefore, the UICollectionViewLayout) by:
//from MyHeaderReusableView.m
if ([self.superview isKindOfClass:UICollectionView.class]) {
//get collectionView reference
UICollectionView * collectionView = (UICollectionView*)self.superview;
//layout
UICollectionViewFlowLayout * layout = (UICollectionViewFlowLayout *)collectionView.collectionViewLayout;
//... perform the header size change
}

Why a frame of a UIView is not updating in ViewDidLayoutSubviews?

I am trying to update the frame of a UIView which contains buttons and labels inside. I am trying to update it in viewDidLayoutSubviews (and I also tried in viewDidLoad, viewWillAppear, viewDidAppear..). I want to change the y position (origin.y) of the view.
The NSLogs says my original y position is 334, and after changing, it is 100. However, the position does not change in my view. I have already checked that the view is connected in the storyboard. What am I doing wrong?
-(void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
CGRect theFrame = [self.bottomView frame];
NSLog(#"Y position bottomview: %f", self.bottomView.frame.origin.y);
if([[UIScreen mainScreen] bounds].size.height == 568) //iPhone 4inch
{
// NSLog(#"iphone5");
}
else{
// NSLog(#"iphone4");
theFrame .origin.y = 100;
}
self.bottomView.frame = theFrame;
NSLog(#"Y position bottomview after changing it: %f", self.bottomView.frame.origin.y);
[self.view layoutIfNeeded];
}
I've had the same problem. Forcing the layouting for your view's superview helped me out:
-(void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
[self.bottomView.superview setNeedsLayout];
[self.bottomView.superview layoutIfNeeded];
// Now modify bottomView's frame here
}
In Swift:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
bottomView.superview!.setNeedsLayout()
bottomView.superview!.layoutIfNeeded()
// Now modify bottomView's frame here
}
Believe it or not the below code fixed it
override func viewDidLayoutSubviews() {
DispatchQueue.main.async {
// UI changes
}
}
The problem was related with Autolayout. However I couldn't turn it off since I am using autolayout in my project. I solved defining appropriate constraints in the view. Then there is no need to check if it is iPhone 4inch or 3.5inch and change the position of the frame since it automatically adapts to each size.
The frame setting should work in your code. But if the view has autolayout constraints (which I assume you have), your frame setting won't work. You can only go one way or the other (manual frame setting or autolayout), not both.

UISearchBar above UICollectionView, how can I get it working with only item in the collection?

I have a UICollectionView that contains a grid of objects. Above it, in a UICollectionView sectionHeader I have a UISearchBar. I want the search bar to be hidden when the view loads. I try to do it with the following code:
- (void)viewDidLoad
{
[super viewDidLoad];
// 44 = height of search bar.
[self.collectionView setContentOffset:CGPointMake(0, 44) animated:YES];
}
This works when the collectionView contains many objects, when the scrollView have scrollIndicators. But when I e.g. only have one item this doesn't work, the searchBar is always visible. I wonder which approach is the best for achieving my goal, display the UISearchBar when the user scrolls down?
Any suggestions?
If you want to display UISearchBar even user scrolling down down then you can do this by:
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
UISearchBar *tempSearchBar = searchDisplayController.preSetSearchBar;
CGRect rect = tempSearchBar.frame;
rect.origin.y = MIN(0, scrollView.contentOffset.y);
tempsearchBar.frame = rect;
}
And if you want to hide it then you only need to use:
[scrollView alwaysBounceVertical:YES]; // allows always bounce to vertical
It is the default behave if UIScrollbar So by setting value it pretends to collection view that it has some more height then view.
From reference:
If this property is set to YES and bounces is YES, vertical dragging
is allowed even if the content is smaller than the bounds of the
scroll view. The default value is NO.
I think you have this problem because of your content collectionView size. Try to increase it before you set contentOffset:
- (void)viewDidLoad
{
[super viewDidLoad];
self.collectionView.contentSize = CGSizeMake(self.view.frame.size.width, self.view.frame.size.height);
[self.collectionView setContentOffset:CGPointMake(0, 44) animated:YES];
}
You can hide your header view by setting it's size to zero
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section
{
if (headerIsActive == NO)
return CGSizeMake(0, 0);
else
return CGSizeMake(768, 44);
}

UICollectionView automatically scroll to bottom when screen loads

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.

UIScrollView adjusts contentOffset when contentSize changes

I am adjusting a detail view controller's state, just before it is pushed on a navigationController:
[self.detailViewController detailsForObject:someObject];
[self.navigationController pushViewController:self.detailViewController
animated:YES];
In the DetailViewController a scrollView resides. Which content I resize based on the passed object:
- (void)detailsForObject:(id)someObject {
// set some textView's content here
self.contentView.frame = <rect with new calculated size>;
self.scrollView.contentSize = self.contentView.frame.size;
self.scrollView.contentOffset = CGPointZero;
}
Now, this all works, but the scrollView adjusts it's contentOffset during the navigationController's slide-in animation. The contentOffset will be set to the difference between the last contentSize and the new calculated one. This means that the second time you open the detailsView, the details will scroll to some unwanted location. Even though I'm setting the contentOffset to CGPointZero explicitly.
I found that resetting the contentOffset in - viewWillAppear has no effect. The best I could come up with is resetting the contentOffset in viewDidAppear, causing a noticeable up and down movement of the content:
- (void)viewDidAppear:(BOOL)animated {
self.scrollView.contentOffset = CGPointZero;
}
Is there a way to prevent a UIScrollView from adjusting its contentOffset when its contentSize is changed?
Occurs when pushing a UIViewController containing a UIScrollView using a UINavigationController.
iOS 11+
Solution 1 (Swift Code):
scrollView.contentInsetAdjustmentBehavior = .never
Solution 2 (Storyboard)
iOS 7
Solution 1 (Code)
Set #property(nonatomic, assign) BOOL automaticallyAdjustsScrollViewInsets to NO.
Solution 2 (Storyboard)
Uncheck the Adjust Scroll View Insets
iOS 6
Solution (Code)
Set the UIScrollView's property contentOffset and contentInset in viewWillLayoutSubviews. Sample code:
- (void)viewWillLayoutSubviews{
[super viewWillLayoutSubviews];
self.scrollView.contentOffset = CGPointZero;
self.scrollView.contentInset = UIEdgeInsetsZero;
}
The cause of this problem remains unclear, though I've found a solution. By resetting the content size and offset before adjusting them, the UIScrollView won't animate:
- (void)detailsForObject:(id)someObject {
// These 2 lines solve the issue:
self.scrollView.contentSize = CGSizeZero;
self.scrollView.contentOffset = CGPointZero;
// set some textView's content here
self.contentView.frame = <rect with new calculated size>;
self.scrollView.contentSize = self.contentView.frame.size;
self.scrollView.contentOffset = CGPointZero;
}
I had the same issue with a UIScrollview, where the problem was caused by not setting the contentSize. After setting the contentSize to the number of items this problem was solved.
self.headerScrollView.mainScrollview.contentSize = CGSizeMake(320 * self.sortedMaterial.count, 0);
Here's what worked for me:
In the storyboard, in the Size Inspector for the scrollView, set Content Insets Adjustment Behavior to "Never".
Is your scrollView the root view of the DetailViewController? If yes, try wrapping the scrollView in a plain UIView and make the latter the root view of DetailViewController. Since UIViews don't have a contentOffset property, they are immune to content offset adjustments made by the navigation controller (due to the navigation bar, etc.).
I experienced the problem, and for a specific case - I don't adjust the size - I used the following:
float position = 100.0;//for example
SmallScroll.center = CGPointMake(position + SmallScroll.frame.size.width / 2.0, SmallScroll.center.y);
Same would work with y: anotherPosition + SmallScroll.frame.size.height / 2.0
So if you don't need to resize, this is a quick and painless solution.
I was experiencing a similar problem, where UIKit was setting the contentOffset of my scrollView during push animations.
None of these solutions were working for me, maybe because I was supporting iOS 10 and iOS 11.
I was able to fix my issue by subclassing my scrollview to keep UIKit from changing my offsets after the scrollview had been removed from the window:
/// A Scrollview that only allows the contentOffset to change while it is in the window hierarchy. This can keep UIKit from resetting the `contentOffset` during transitions, etc.
class LockingScrollView: UIScrollView {
override var contentOffset: CGPoint {
get {
return super.contentOffset
}
set {
if window != nil {
super.contentOffset = newValue
}
}
}
}
Adding to KarenAnne's answer:
iOS 11+
automaticallyAdjustsScrollViewInsets was deprecated
Use this istead:
Storyboards:
Code (Swift):
scrollView.contentInsetAdjustmentBehavior = .never

Resources