UICollectionView insert cells above maintaining position (like Messages.app)

后端 未结 20 2026
渐次进展
渐次进展 2020-12-02 07:23

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

相关标签:
20条回答
  • 2020-12-02 08:16

    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];
    }
    
    0 讨论(0)
  • 2020-12-02 08:19

    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
    }
    
    0 讨论(0)
  • 2020-12-02 08:23

    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()
    })
    
    0 讨论(0)
  • 2020-12-02 08:23

    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.

    0 讨论(0)
  • 2020-12-02 08:23

    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!

    0 讨论(0)
  • 2020-12-02 08:23
    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;
    
    0 讨论(0)
提交回复
热议问题