By default Collection View maintains content offset while inserting cells. On the other hand I'd like to insert cells above the currently displaying ones so that they appear above the screen top edge like Messages.app do when you load earlier messages. Does anyone know the way to achieve it?
20 Answers
This is the technique I use. I've found others cause strange side effects such as screen flicker:
CGFloat bottomOffset = self.collectionView.contentSize.height - self.collectionView.contentOffset.y;
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self.collectionView performBatchUpdates:^{
[self.collectionView insertItemsAtIndexPaths:indexPaths];
} completion:^(BOOL finished) {
self.collectionView.contentOffset = CGPointMake(0, self.collectionView.contentSize.height - bottomOffset);
}];
[CATransaction commit];

- 545
- 5
- 13

- 1,050
- 7
- 8
-
Works great, thanks! Please see below the version in Swift 2. – Sebastian Sep 21 '15 at 09:31
-
Your solution does not work (unfortunately) if I do `[self.tableView reloadItemsAtIndexPaths:indexPathsToUpdate];` with changes in the layout. – Colas Oct 20 '15 at 15:31
-
2Works great just a very slight flicker on update, not bad though. Best I have found aside from flipping whole thing upside down. When I did the flip though it made my collection view very laggy because it has a lot of stuff in it. So for me this was best. – Wayne Filkins Oct 17 '16 at 22:54
-
what is indexpaths ? – Shikha Sharma Apr 07 '17 at 13:47
-
To remove the flicker, you can try method of @Peter Stajger – shellhue Jul 20 '17 at 09:50
James Martin’s fantastic version converted to Swift 2:
let amount = 5 // change this to the amount of items to add
let section = 0 // change this to your needs, too
let contentHeight = self.collectionView!.contentSize.height
let offsetY = self.collectionView!.contentOffset.y
let bottomOffset = contentHeight - offsetY
CATransaction.begin()
CATransaction.setDisableActions(true)
self.collectionView!.performBatchUpdates({
var indexPaths = [NSIndexPath]()
for i in 0..<amount {
let index = 0 + i
indexPaths.append(NSIndexPath(forItem: index, inSection: section))
}
if indexPaths.count > 0 {
self.collectionView!.insertItemsAtIndexPaths(indexPaths)
}
}, completion: {
finished in
print("completed loading of new stuff, animating")
self.collectionView!.contentOffset = CGPointMake(0, self.collectionView!.contentSize.height - bottomOffset)
CATransaction.commit()
})

- 8,952
- 3
- 32
- 30
-
1You can do: let indexPaths = (0...amount).map{NSIndexPath(forItem: $0, inSection: section)} – Jon Andersen Jan 10 '16 at 22:33
-
My approach leverages subclassed flow layout. This means that you don't have to hack scrolling/layout code in a view controller. Idea is that whenever you know that you are inserting cells on top you set custom property you flag that next layout update will be inserting cells to top and you remember content size before update. Then you override prepareLayout() and set desired content offset there. It looks something like this:
define variables
private var isInsertingCellsToTop: Bool = false
private var contentSizeWhenInsertingToTop: CGSize?
override prepareLayout()
and after calling super
if isInsertingCellsToTop == true {
if let collectionView = collectionView, oldContentSize = contentSizeWhenInsertingToTop {
let newContentSize = collectionViewContentSize()
let contentOffsetY = collectionView.contentOffset.y + (newContentSize.height - oldContentSize.height)
let newOffset = CGPointMake(collectionView.contentOffset.x, contentOffsetY)
collectionView.setContentOffset(newOffset, animated: false)
}
contentSizeWhenInsertingToTop = nil
isInsertingMessagesToTop = false
}

- 1,685
- 17
- 26
-
3
-
1Works for me only when inserting the new section using UIView.performWithoutAnimation({self.collectionView?.insertSections(NSIndexSet(index: 0))}) but scrolling is suddenly stopped. Is there a way to achieve continuous scrolling i.e. "silently" inserting sections above the CollectionView's bounds? – RTasche Jan 05 '16 at 19:21
-
@hacker2007 try using collectionView.contentOffset = newOffset instead of the setContentOffset This worked for me. – Jon Andersen Jan 20 '16 at 00:21
-
1Great approach. There are a few minor issues with your code as of May 30 2017 - i've posted a Swift 3 version of your answer below. Thanks Peter! – xaphod May 30 '17 at 20:42
-
This is by far the cleaner and concise solution i have found about this. Thanks for sharing. – Javier Cadiz Oct 24 '17 at 12:32
-
I did this in two lines of code (although it was on a UITableView) but I think you'd be able to do it the same way.
I rotated the tableview 180 degrees.
Then I rotated each tableview cell by 180 degrees also.
This meant that I could treat it as a standard top to bottom table but the bottom was treated like the top.

- 76,236
- 42
- 207
- 306
-
5
-
@k06a care to explain why? You would still have to scale the cells too by (1, -1) too. Isn't it the same? – Fogmeister Oct 21 '14 at 15:47
-
3@Fogmeister its better to scale the table (1,-1) and cells (1,-1). If you rotate, you also flip the scroll indicator to the other side of the screen. Reflecting on the x axis maintains the scroll indicator. – Andy Poes Oct 27 '16 at 18:14
-
@AndyPoes ah nice. In my app I didn't need the scroll indicator but yeah. That definitely makes sense :-) – Fogmeister Oct 27 '16 at 18:21
-
@k06a awesome. didn't notice that until I read your comment. double thumbs up – 0xKayvan Jan 31 '17 at 09:24
Swift 3 version code: based on James Martin answer
let amount = 1 // change this to the amount of items to add
let section = 0 // change this to your needs, too
let contentHeight = self.collectionView.contentSize.height
let offsetY = self.collectionView.contentOffset.y
let bottomOffset = contentHeight - offsetY
CATransaction.begin()
CATransaction.setDisableActions(true)
self.collectionView.performBatchUpdates({
var indexPaths = [NSIndexPath]()
for index in 0..<amount {
indexPaths.append(NSIndexPath(item: index, section: section))
}
if indexPaths.count > 0 {
self.collectionView.insertItems(at: indexPaths as [IndexPath])
}
}, completion: {
finished in
print("completed loading of new stuff, animating")
self.collectionView.contentOffset = CGPoint(x: 0, y: self.collectionView.contentSize.height - bottomOffset)
CATransaction.commit()
})

- 5
- 5

- 1,887
- 23
- 11
-
-
Any idea why `CATransaction.setDisableActions(true)` is important here? – Shawn Throop Aug 21 '22 at 16:01
Here's a slightly tweaked version of Peter's solution (subclassing flow layout, no upside-down, lightweight approach). It's Swift 3. Note UIView.animate
with zero duration - that's to allow the animation of the even/oddness of the cells (what's on a row) animate, but stop the animation of the viewport offset changing (which would look terrible)
Usage:
let layout = self.collectionview.collectionViewLayout as! ContentSizePreservingFlowLayout
layout.isInsertingCellsToTop = true
self.collectionview.performBatchUpdates({
if let deletionIndexPaths = deletionIndexPaths, deletionIndexPaths.count > 0 {
self.collectionview.deleteItems(at: deletionIndexPaths.map { return IndexPath.init(item: $0.item+twitterItems, section: 0) })
}
if let insertionIndexPaths = insertionIndexPaths, insertionIndexPaths.count > 0 {
self.collectionview.insertItems(at: insertionIndexPaths.map { return IndexPath.init(item: $0.item+twitterItems, section: 0) })
}
}) { (finished) in
completionBlock?()
}
Here's ContentSizePreservingFlowLayout
in its entirety:
class ContentSizePreservingFlowLayout: UICollectionViewFlowLayout {
var isInsertingCellsToTop: Bool = false {
didSet {
if isInsertingCellsToTop {
contentSizeBeforeInsertingToTop = collectionViewContentSize
}
}
}
private var contentSizeBeforeInsertingToTop: CGSize?
override func prepare() {
super.prepare()
if isInsertingCellsToTop == true {
if let collectionView = collectionView, let oldContentSize = contentSizeBeforeInsertingToTop {
UIView.animate(withDuration: 0, animations: {
let newContentSize = self.collectionViewContentSize
let contentOffsetY = collectionView.contentOffset.y + (newContentSize.height - oldContentSize.height)
let newOffset = CGPoint(x: collectionView.contentOffset.x, y: contentOffsetY)
collectionView.contentOffset = newOffset
})
}
contentSizeBeforeInsertingToTop = nil
isInsertingCellsToTop = false
}
}
}

- 6,392
- 2
- 37
- 45
-
1This worked for me :) Just a couple of things to mention: "if isInsertingCellsToTop == true" can simple read "if isInsertingCellsToTop" 'cause it's a Bool and I replaced "UIView.animate(withDuration: 0, animations:" with UIView.performWithoutAnimation. – Neil Billingham Sep 28 '17 at 08:48
Adding to Fogmeister's answer (with code), the cleanest approach is to invert (turn upside-down) the UICollectionView
so that you have a scroll view that is sticky to the bottom rather than the top. This also works for UITableView
, as Fogmeister points out.
- (void)viewDidLoad
{
[super viewDidLoad];
self.collectionView.transform = CGAffineTransformMake(1, 0, 0, -1, 0, 0);
}
In Swift:
override func viewDidLoad() {
super.viewDidLoad()
collectionView.transform = CGAffineTransformMake(1, 0, 0, -1, 0, 0)
}
This has the side effect of also displaying your cells upside-down so you have to flip those as well. So we transfer the trasform (cell.transform = collectionView.transform
) like so:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
cell.transform = collectionView.transform;
return cell;
}
In Swift:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! UICollectionViewCell
cell.transform = collectionView.transform
return cell
}
Lastly, the main thing to remember when developing under this design is that the NSIndexPath
parameters in delegates are reversed. So indexPath.row == 0
is the row at on the bottom of the collectionView
where it is normally at the top.
This technique is used in many open source projects to produce the behavior described including the popular SlackTextViewController (https://github.com/slackhq/SlackTextViewController) maintained by Slack
Thought I would add some code context to Fogmeister's fantastic answer!

- 1,977
- 3
- 14
- 30

- 5,458
- 2
- 27
- 22
-
1This approach works very well for static content, but when calling self.collectionView.insertItemsAtIndexPaths(indexPaths) the collectionView performs some animations which cause the cells to be flipped to their original direction. Did you ever run into this and managed to solve it? – Jon Andersen Jan 14 '16 at 16:10
-
@JonAndersen I havent experienced this. you have control over these animations: (https://www.objc.io/issues/12-animations/collectionview-animations/). are u using a custom one? if not do you need the animation? you can try removing or changing the animation to one that doesnt effect the transform (ie fade in). – mattr Jan 14 '16 at 16:20
-
I tried with with a custom layout without any animations, however it still has some animations that I'm not able to find and stop. Thanks – Jon Andersen Jan 25 '16 at 21:55
-
@JonAndersen did you ever figure this out? I'm having the same issue you described when using insertItemsAtIndexPaths and can't fix it. Tried creating a subclass of UICollectionViewFlowLayout and overriding the initialLayoutAttributesForAppearing with the same transform as collection view.. didn't do a thing. Thanks – Walker Apr 01 '17 at 17:09
-
@Walker I don't think I was satisfied enough with the solution. I started using Async Display Kit, and that solved all my issues with rendering. – Jon Andersen Apr 03 '17 at 17:26
-
@JonAndersen do you mine posting some code or pointing me in the right direction? I really appreciate it man – Walker Apr 03 '17 at 17:27
-
@Walker I don't have any code right now. But AsyncDisplayKit has a propety on UITableView & UICollectionView which works perfectly. http://asyncdisplaykit.org/docs/inversion.html – Jon Andersen Apr 10 '17 at 14:28
-
1Setting `UICollectionViewCell.transform` didn't work for me as animations (`animateWithDuration`/`performBatchUpdates`) trampled the transform. Instead, setting `UICollectionViewCell.contentView.transform` worked better. – jedwidz Oct 23 '18 at 06:27
-
A side-effect of this technique is that tapping on the status bar scrolls to the bottom of the view rather than the top. (See docs for property `scrollsToTop` to disable or customize this.) – jedwidz Jan 03 '19 at 09:07
This is what I learned from JSQMessagesViewController: How maintain scroll position?. Very simple, useful and NO flicker!
// Update collectionView dataSource
data.insert(contentsOf: array, at: startRow)
// Reserve old Offset
let oldOffset = self.collectionView.contentSize.height - self.collectionView.contentOffset.y
// Update collectionView
collectionView.reloadData()
collectionView.layoutIfNeeded()
// Restore old Offset
collectionView.contentOffset = CGPoint(x: 0, y: self.collectionView.contentSize.height - oldOffset)

- 99
- 5
-
-
1works when the user isn't scrolling but when scrolling this causes problems – Lance Samaria Nov 29 '20 at 15:33
Love James Martin’s solution. But for me it started to breakdown when inserting/deleting above/below a specific content window. I took a stab at subclassing UICollectionViewFlowLayout to get the behavior I wanted. Hope this helps someone. Any feedback appreciated :)
@interface FixedScrollCollectionViewFlowLayout () {
__block float bottomMostVisibleCell;
__block float topMostVisibleCell;
}
@property (nonatomic, assign) BOOL isInsertingCellsToTop;
@property (nonatomic, strong) NSArray *visableAttributes;
@property (nonatomic, assign) float offset;;
@end
@implementation FixedScrollCollectionViewFlowLayout
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
_isInsertingCellsToTop = NO;
}
return self;
}
- (id)init {
self = [super init];
if (self) {
_isInsertingCellsToTop = NO;
}
return self;
}
- (void)prepareLayout {
NSLog(@"prepareLayout");
[super prepareLayout];
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSLog(@"layoutAttributesForElementsInRect");
self.visableAttributes = [super layoutAttributesForElementsInRect:rect];
self.offset = 0;
self.isInsertingCellsToTop = NO;
return self.visableAttributes;
}
- (void)prepareForCollectionViewUpdates:(NSArray *)updateItems {
bottomMostVisibleCell = -MAXFLOAT;
topMostVisibleCell = MAXFLOAT;
CGRect container = CGRectMake(self.collectionView.contentOffset.x, self.collectionView.contentOffset.y, self.collectionView.frame.size.width, self.collectionView.frame.size.height);
[self.visableAttributes enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *attributes, NSUInteger idx, BOOL *stop) {
CGRect currentCellFrame = attributes.frame;
CGRect containerFrame = container;
if(CGRectIntersectsRect(containerFrame, currentCellFrame)) {
float x = attributes.indexPath.row;
if (x < topMostVisibleCell) topMostVisibleCell = x;
if (x > bottomMostVisibleCell) bottomMostVisibleCell = x;
}
}];
NSLog(@"prepareForCollectionViewUpdates");
[super prepareForCollectionViewUpdates:updateItems];
for (UICollectionViewUpdateItem *updateItem in updateItems) {
switch (updateItem.updateAction) {
case UICollectionUpdateActionInsert:{
NSLog(@"UICollectionUpdateActionInsert %ld",updateItem.indexPathAfterUpdate.row);
if (topMostVisibleCell>updateItem.indexPathAfterUpdate.row) {
UICollectionViewLayoutAttributes * newAttributes = [self layoutAttributesForItemAtIndexPath:updateItem.indexPathAfterUpdate];
self.offset += (newAttributes.size.height + self.minimumLineSpacing);
self.isInsertingCellsToTop = YES;
}
break;
}
case UICollectionUpdateActionDelete: {
NSLog(@"UICollectionUpdateActionDelete %ld",updateItem.indexPathBeforeUpdate.row);
if (topMostVisibleCell>updateItem.indexPathBeforeUpdate.row) {
UICollectionViewLayoutAttributes * newAttributes = [self layoutAttributesForItemAtIndexPath:updateItem.indexPathBeforeUpdate];
self.offset -= (newAttributes.size.height + self.minimumLineSpacing);
self.isInsertingCellsToTop = YES;
}
break;
}
case UICollectionUpdateActionMove:
NSLog(@"UICollectionUpdateActionMoveB %ld", updateItem.indexPathBeforeUpdate.row);
break;
default:
NSLog(@"unhandled case: %ld", updateItem.indexPathBeforeUpdate.row);
break;
}
}
if (self.isInsertingCellsToTop) {
if (self.collectionView) {
[CATransaction begin];
[CATransaction setDisableActions:YES];
}
}
}
- (void)finalizeCollectionViewUpdates {
CGPoint newOffset = CGPointMake(self.collectionView.contentOffset.x, self.collectionView.contentOffset.y + self.offset);
if (self.isInsertingCellsToTop) {
if (self.collectionView) {
self.collectionView.contentOffset = newOffset;
[CATransaction commit];
}
}
}

- 31
- 2
Inspired by Bryan Pratte's solution I developed subclass of UICollectionViewFlowLayout to get chat behavior without turning collection view upside-down. This layout is written in Swift 3 and absolutely usable with RxSwift and RxDataSources because UI is completely separated from any logic or binding.
Three things were important for me:
- If there is a new message, scroll down to it. It doesn't matter where you are in the list in this moment. Scrolling is realized with
setContentOffset
instead ofscrollToItemAtIndexPath
. - If you do "Lazy Loading" with older messages, then the scroll view shouldn't change and stays exactly where it is.
- Add exceptions for the beginning. The collection view should behave "normal" till there are more messages than space on the screen.
My solution: https://gist.github.com/jochenschoellig/04ffb26d38ae305fa81aeb711d043068

- 137
- 1
- 6
-
Works awesomely (even with refreshControl present). Thanks a lot ! +1 – HelloimDarius May 18 '17 at 08:24
-
Looks good! I'm implementing a change in our chat app using this code: https://github.com/RocketChat/Rocket.Chat.iOS/pull/577. Thank you! – Rafael Kellermann Streit Aug 22 '17 at 20:58
While all solutions above are worked for me, the main reason of those to fail is that when user is scrolling while those items are being added, scroll will either stop or there'll be noticeable lag Here is a solution that helps to maintain (visual)scroll position while adding items to the top.
class Layout: UICollectionViewFlowLayout {
var heightOfInsertedItems: CGFloat = 0.0
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
var offset = proposedContentOffset
offset.y += heightOfInsertedItems
heightOfInsertedItems = 0.0
return offset
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
var offset = proposedContentOffset
offset.y += heightOfInsertedItems
heightOfInsertedItems = 0.0
return offset
}
override func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) {
super.prepare(forCollectionViewUpdates: updateItems)
var totalHeight: CGFloat = 0.0
updateItems.forEach { item in
if item.updateAction == .insert {
if let index = item.indexPathAfterUpdate {
if let attrs = layoutAttributesForItem(at: index) {
totalHeight += attrs.frame.height
}
}
}
}
self.heightOfInsertedItems = totalHeight
}
}
This layout remembers the height of items those are about to be inserted, and then next time, when layout will be asked for offset, it will compensate offset by the height of added items.

- 5,954
- 2
- 33
- 51
Not the most elegant but quite simple and working solution I stuck with for now. Works only with linear layout (not grid) but it's fine for me.
// retrieve data to be inserted
NSArray *fetchedObjects = [managedObjectContext executeFetchRequest:fetchRequest error:nil];
NSMutableArray *objects = [fetchedObjects mutableCopy];
[objects addObjectsFromArray:self.messages];
// self.messages is a DataSource array
self.messages = objects;
// calculate index paths to be updated (we are inserting
// fetchedObjects.count of objects at the top of collection view)
NSMutableArray *indexPaths = [NSMutableArray new];
for (int i = 0; i < fetchedObjects.count; i ++) {
[indexPaths addObject:[NSIndexPath indexPathForItem:i inSection:0]];
}
// calculate offset of the top of the displayed content from the bottom of contentSize
CGFloat bottomOffset = self.collectionView.contentSize.height - self.collectionView.contentOffset.y;
// performWithoutAnimation: cancels default collection view insertion animation
[UIView performWithoutAnimation:^{
// capture collection view image representation into UIImage
UIGraphicsBeginImageContextWithOptions(self.collectionView.bounds.size, NO, 0);
[self.collectionView drawViewHierarchyInRect:self.collectionView.bounds afterScreenUpdates:YES];
UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// place the captured image into image view laying atop of collection view
self.snapshot.image = snapshotImage;
self.snapshot.hidden = NO;
[self.collectionView performBatchUpdates:^{
// perform the actual insertion of new cells
[self.collectionView insertItemsAtIndexPaths:indexPaths];
} completion:^(BOOL finished) {
// after insertion finishes, scroll the collection so that content position is not
// changed compared to such prior to the update
self.collectionView.contentOffset = CGPointMake(0, self.collectionView.contentSize.height - bottomOffset);
[self.collectionView.collectionViewLayout invalidateLayout];
// and hide the snapshot view
self.snapshot.hidden = YES;
}];
}];

- 3,469
- 4
- 25
- 35
-
I tried this way to scroll to next item after loading more datas on scroll. It is not working, it scrolls my UICollectionview to top @mrvn – Mansuu.... Nov 07 '17 at 10:25
-
if ([newMessages count] > 0)
{
[self.collectionView reloadData];
if (hadMessages)
[self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:[newMessages count] inSection:0] atScrollPosition:UICollectionViewScrollPositionTop animated:NO];
}
This seems to be working so far. Reload the collection, scroll the previously first message to the top without animation.

- 958
- 9
- 11
I managed to write a solution which works for cases when inserting cells at the top and bottom at the same time.
- Save the position of the top visible cell. Compute the height of the cell which is underneath the navBar (the top view. in my case it is the self.participantsView)
// get the top cell and save frame
NSMutableArray<NSIndexPath*> *visibleCells = [self.collectionView indexPathsForVisibleItems].mutableCopy;
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"item" ascending:YES];
[visibleCells sortUsingDescriptors:@[sortDescriptor]];
ChatMessage *m = self.chatMessages[visibleCells.firstObject.item];
UICollectionViewCell *topCell = [self.collectionView cellForItemAtIndexPath:visibleCells.firstObject];
CGRect topCellFrame = topCell.frame;
CGRect navBarFrame = [self.view convertRect:self.participantsView.frame toView:self.collectionView];
CGFloat offset = CGRectGetMaxY(navBarFrame) - topCellFrame.origin.y;
- Reload your data.
[self.collectionView reloadData];
- Get the new position of the item. Get the attributes for that index. Extract the offset and change contentOffset of the collectionView.
// scroll to the old cell position
NSUInteger messageIndex = [self.chatMessages indexOfObject:m];
UICollectionViewLayoutAttributes *attr = [self.collectionView layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:messageIndex inSection:0]];
self.collectionView.contentOffset = CGPointMake(0, attr.frame.origin.y + offset);

- 162
- 6
// stop scrolling
setContentOffset(contentOffset, animated: false)
// calculate the offset and reloadData
let beforeContentSize = contentSize
reloadData()
layoutIfNeeded()
let afterContentSize = contentSize
// reset the contentOffset after data is updated
let newOffset = CGPoint(
x: contentOffset.x + (afterContentSize.width - beforeContentSize.width),
y: contentOffset.y + (afterContentSize.height - beforeContentSize.height))
setContentOffset(newOffset, animated: false)

- 739
- 1
- 11
- 21
I found the five steps work seamlessly:
Prepare data for your new cells, and insert the data as appropriate
Tell
UIView
to stop animationUIView.setAnimationsEnabled(false)
Actually insert those cells
collectionView?.insertItems(at: indexPaths)
Scroll the collection view (which is a subclass of
UIScrollView
)scrollView.contentOffset.y += CELL_HEIGHT * CGFloat(ITEM_COUNT)
Notice to substitute CELL_HEIGHT with the height of your cells (which is only easy if cells are of a fixed size). It is important to add any cell-to-cell margin / insets.
Remember to tell
UIView
to start animation again:UIView.setAnimationsEnabled(true)

- 297
- 3
- 17
A few of the suggested approaches had varying degrees of success for me. I eventually used a variation of the subclassing and prepareLayout
option Peter Stajger putting my offset correction in finalizeCollectionViewUpdates
. However today as I was looking at some additional documentation I found targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint)
and I think that feels a lot more like the intended location for this type of correction. So this is my implementation using that. Note my implmentation was for a horizontal collection but cellsInsertingToTheLeft
could be easily updated as cellsInsertingAbove
and the offset corrected accordingly.
class GCCFlowLayout: UICollectionViewFlowLayout {
var cellsInsertingToTheLeft: Int?
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
guard let cells = cellsInsertingToTheLeft else { return proposedContentOffset }
guard let collectionView = collectionView else { return proposedContentOffset }
let contentOffsetX = collectionView.contentOffset.x + CGFloat(cells) * (collectionView.bounds.width - 45 + 8)
let newOffset = CGPoint(x: contentOffsetX, y: collectionView.contentOffset.y)
cellsInsertingToTheLeft = nil
return newOffset
}
}

- 525
- 1
- 5
- 5
Based on @Steven answer, I managed to make insert cell with scroll to the bottom, without any flickering (and using auto cells), tested on iOS 12
let oldOffset = self.collectionView!.contentOffset
let oldOffsetDelta = self.collectionView!.contentSize.height - self.collectionView!.contentOffset.y
CATransaction.begin()
CATransaction.setCompletionBlock {
self.collectionView!.setContentOffset(CGPoint(x: 0, y: self.collectionView!.contentSize.height - oldOffsetDelta), animated: true)
}
collectionView!.reloadData()
collectionView!.layoutIfNeeded()
self.collectionView?.setContentOffset(oldOffset, animated: false)
CATransaction.commit()

- 695
- 5
- 23
I have used the @James Martin approach, but if you use coredata
and NSFetchedResultsController
the right approach is store the number of earlier messages loaded in _earlierMessagesLoaded
and check the value in the controllerDidChangeContent:
#pragma mark - NSFetchedResultsController
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
if(_earlierMessagesLoaded)
{
__block NSMutableArray * indexPaths = [NSMutableArray new];
for (int i =0; i<[_earlierMessagesLoaded intValue]; i++)
{
[indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
}
CGFloat bottomOffset = self.collectionView.contentSize.height - self.collectionView.contentOffset.y;
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self.collectionView performBatchUpdates:^{
[self.collectionView insertItemsAtIndexPaths:indexPaths];
} completion:^(BOOL finished) {
self.collectionView.contentOffset = CGPointMake(0, self.collectionView.contentSize.height - bottomOffset);
[CATransaction commit];
_earlierMessagesLoaded = nil;
}];
}
else
[self finishReceivingMessageAnimated:NO];
}

- 1,977
- 3
- 14
- 30

- 9,217
- 2
- 29
- 41
CGPoint currentOffset = _collectionView.contentOffset;
CGSize contentSizeBeforeInsert = [_collectionView.collectionViewLayout collectionViewContentSize];
[_collectionView reloadData];
CGSize contentSizeAfterInsert = [_collectionView.collectionViewLayout collectionViewContentSize];
CGFloat deltaHeight = contentSizeAfterInsert.height - contentSizeBeforeInsert.height;
currentOffset.y += MAX(deltaHeight, 0);
_collectionView.contentOffset = currentOffset;

- 129
- 5