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

后端 未结 20 2024
渐次进展
渐次进展 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:00

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

    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.

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

    I found the five steps work seamlessly:

    1. Prepare data for your new cells, and insert the data as appropriate

    2. Tell UIView to stop animation

      UIView.setAnimationsEnabled(false)
      
    3. Actually insert those cells

      collectionView?.insertItems(at: indexPaths)
      
    4. 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.

    5. Remember to tell UIView to start animation again:

      UIView.setAnimationsEnabled(true)
      
    0 讨论(0)
  • 2020-12-02 08:07

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

    I managed to write a solution which works for cases when inserting cells at the top and bottom at the same time.

    1. 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;
    
    1. Reload your data.
    [self.collectionView reloadData];
    
    1. 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);
    
    0 讨论(0)
  • 2020-12-02 08:07

    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
        }
    }
    
    0 讨论(0)
提交回复
热议问题