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 appea
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];
}
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
}
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()
})
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.
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!
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;