UICollectionView 在保持位置上方插入单元格(如 Messages.app)

Posted

技术标签:

【中文标题】UICollectionView 在保持位置上方插入单元格(如 Messages.app)【英文标题】:UICollectionView insert cells above maintaining position (like Messages.app) 【发布时间】:2014-08-28 11:58:35 【问题描述】:

默认情况下,集合视图在插入单元格时保持内容偏移。另一方面,我想在当前显示的单元格上方插入单元格,以便它们出现在屏幕顶部边缘上方,就像 Messages.app 在您加载早期消息时所做的那样。有谁知道实现它的方法吗?

【问题讨论】:

【参考方案1】:

这是我使用的技术。我发现其他人会导致奇怪的副作用,例如屏幕闪烁:

    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];

【讨论】:

效果很好,谢谢!请参阅下面的 Swift 2 版本。 如果我对布局进行更改[self.tableView reloadItemsAtIndexPaths:indexPathsToUpdate];,您的解决方案将不起作用(不幸的是)。 效果很好,只是更新时有轻微的闪烁,但还不错。除了把整个事情颠倒过来之外,我发现的最好的。当我进行翻转时,它使我的收藏视图非常滞后,因为它里面有很多东西。所以对我来说这是最好的。 什么是索引路径? 去除闪烁可以试试@Peter Stajger的方法【参考方案2】:

James Martin 的精彩版本转换为 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()
)

【讨论】:

你可以这样做:让 indexPaths = (0...amount).mapNSIndexPath(forItem: $0, inSection: section) 如何隐藏collectionview的滚动??【参考方案3】:

我的方法利用子类流布局。这意味着您不必破解视图控制器中的滚动/布局代码。想法是,只要您知道要在顶部插入单元格,就可以设置自定义属性,然后标记下一次布局更新将在顶部插入单元格,并且您会记住更新前的内容大小。然后覆盖 prepareLayout() 并在那里设置所需的内容偏移量。它看起来像这样:

定义变量

private var isInsertingCellsToTop: Bool = false
private var contentSizeWhenInsertingToTop: CGSize?

覆盖 prepareLayout() 并在调用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

【讨论】:

这可能是迄今为止最好的选择。看起来很有希望 仅在使用 UIView.performWithoutAnimation(self.collectionView?.insertSections(NSIndexSet(index: 0))) 插入新部分时对我有用,但滚动突然停止。有没有办法实现连续滚动,即在 CollectionView 边界上方“静默”插入部分? @hacker2007 尝试使用 collectionView.contentOffset = newOffset 而不是 setContentOffset 这对我有用。 很棒的方法。截至 2017 年 5 月 30 日,您的代码存在一些小问题 - 我在下面发布了您的答案的 Swift 3 版本。谢谢彼得! 这是迄今为止我找到的更简洁简洁的解决方案。感谢分享。【参考方案4】:

我在两行代码中做到了这一点(虽然它是在 UITableView 上),但我认为你可以用同样的方式做到这一点。

我将 tableview 旋转了 180 度。

然后我将每个 tableview 单元格也旋转了 180 度。

这意味着我可以将其视为标准的从上到下的表格,但底部被视为顶部。

【讨论】:

通过 (1,-1) 更好地缩放它 @k06a 想解释一下为什么?您仍然必须将单元格也按 (1, -1) 缩放。不一样吗? @Fogmeister 更好地缩放表格 (1,-1) 和单元格 (1,-1)。如果您旋转,您也会将滚动指示器翻转到屏幕的另一侧。反映在 x 轴上会保持滚动指示器。 @AndyPoes 啊很好。在我的应用程序中,我不需要滚动指示器,但是是的。这绝对是有道理的:-) @k06a 太棒了。直到我读到你的评论才注意到这一点。竖起大拇指【参考方案5】:

这是 Peter 解决方案的略微调整版本(子类化流布局,不颠倒,轻量级方法)。它是 Swift 3。注意UIView.animate 的持续时间为零 - 这是为了允许单元格的偶数/奇数动画(一行上的内容)动画,但停止视口偏移变化的动画(这看起来很糟糕)

用法:

        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?()
        

这里是ContentSizePreservingFlowLayout 的全部内容:

    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
            
        
    

【讨论】:

这对我有用:) 只需要提几件事:“如果 isInsertingCellsToTop == true”可以简单地阅读“如果 isInsertingCellsToTop”,因为它是一个布尔值,我替换了“UIView.animate(withDuration : 0, 动画:" 带有 UIView.performWithoutAnimation。【参考方案6】:

Swift 3 版本代码: 基于 James Martin 的回答

    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()
    )

【讨论】:

CATransaction object 拯救我的一天。很好的解决方案。【参考方案7】:

添加到 Fogmeister 的答案(使用代码),最干净的方法是反转(倒置)UICollectionView,这样您的滚动视图就会粘在底部而不是顶部。正如 Fogmeister 所指出的,这也适用于 UITableView

- (void)viewDidLoad

    [super viewDidLoad];

    self.collectionView.transform = CGAffineTransformMake(1, 0, 0, -1, 0, 0);


在 Swift 中:

override func viewDidLoad() 
    super.viewDidLoad()

    collectionView.transform = CGAffineTransformMake(1, 0, 0, -1, 0, 0)

这有一个副作用是你的单元格也会倒置显示,所以你也必须翻转它们。所以我们像这样传输 trasform (cell.transform = collectionView.transform):

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath

    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];

    cell.transform = collectionView.transform;

    return cell;

在 Swift 中:

func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell 
    var cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! UICollectionViewCell

    cell.transform = collectionView.transform

    return cell

最后,在这种设计下开发时要记住的主要事情是委托中的NSIndexPath 参数是相反的。所以indexPath.row == 0collectionView 底部的行,它通常位于顶部。

许多开源项目都使用这种技术来产生所描述的行为,包括由Slack 维护的流行的 SlackTextViewController (https://github.com/slackhq/SlackTextViewController)

我想我会在 Fogmeister 的精彩回答中添加一些代码上下文!

【讨论】:

这种方法非常适用于静态内容,但是当调用 self.collectionView.insertItemsAtIndexPaths(indexPaths) 时,collectionView 会执行一些动画,导致单元格被翻转到它们的原始方向。你有没有遇到过这个并设法解决它? @JonAndersen 我没有经历过这个。您可以控制这些动画:(objc.io/issues/12-animations/collectionview-animations)。你用的是定制的吗?如果不需要,你需要动画吗?您可以尝试将动画移除或更改为不影响变换的动画(即淡入)。 我尝试使用没有任何动画的自定义布局,但它仍然有一些我无法找到和停止的动画。谢谢 @JonAndersen 你有想过这个吗?我在使用 insertItemsAtIndexPaths 时遇到了与您描述的相同的问题,但无法修复。尝试创建 UICollectionViewFlowLayout 的子类并使用与集合视图相同的转换覆盖 initialLayoutAttributesForAppearing .. 没有做任何事情。谢谢 设置 UICollectionViewCell.transform 对我不起作用,因为动画 (animateWithDuration/performBatchUpdates) 践踏了转换。相反,设置UICollectionViewCell.contentView.transform 效果更好。【参考方案8】:

这是我从JSQMessagesViewController: How maintain scroll position? 学到的。非常简单,好用,无闪烁!

 // 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)

【讨论】:

辉煌——超干净的结果 在用户不滚动但滚动时会出现问题【参考方案9】:

喜欢 James Martin 的解决方案。但对我来说,在特定内容窗口的上方/下方插入/删除时,它开始崩溃。我尝试子类化 UICollectionViewFlowLayout 以获得我想要的行为。希望这可以帮助某人。任何反馈表示赞赏:)

@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];
        
    

【讨论】:

【参考方案10】:

Bryan Pratte 的解决方案的启发,我开发了 UICollectionViewFlowLayout 的子类来获得聊天行为而不会将集合视图倒置。此布局是用 Swift 3 编写的,并且绝对可以与 RxSwiftRxDataSources 一起使用,因为 UI 与任何逻辑或绑定完全分离。

三件事对我来说很重要:

    如果有新消息,请向下滚动到它。此时此刻,您在列表中的哪个位置并不重要。滚动是用setContentOffset而不是scrollToItemAtIndexPath实现的。 如果您对较旧的消息执行“延迟加载”,则滚动视图不应更改并保持在原来的位置。 在开头添加例外。集合视图应该表现“正常”,直到屏幕上的消息多于空间。

我的解决方案: https://gist.github.com/jochenschoellig/04ffb26d38ae305fa81aeb711d043068

【讨论】:

效果很好(即使存在 refreshControl)。非常感谢 ! +1 看起来不错!我正在使用以下代码在我们的聊天应用程序中进行更改:github.com/RocketChat/Rocket.Chat.ios/pull/577。谢谢!【参考方案11】:

虽然上述所有解决方案都对我有用,但失败的主要原因是当用户在添加这些项目时滚动时,滚动要么停止,要么会有明显的延迟 这是一个在将项目添加到顶部时有助于保持(视觉)滚动位置的解决方案。

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
    

这个布局会记住即将插入的item的高度,然后下次layout需要offset的时候,会用添加的item的高度来补偿offset。

【讨论】:

你能提供一个如何使用这个的例子【参考方案12】:

不是我现在坚持的最优雅但非常简单且有效的解决方案。仅适用于线性布局(不是网格),但对我来说没问题。

// 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;
    ];
];

【讨论】:

在滚动加载更多数据后,我尝试使用这种方式滚动到下一项。它不起作用,它将我的 UICollectionview 滚动到顶部 @mrvn 约束违反在这方面有什么作用吗? @mrvn【参考方案13】:
if ([newMessages count] > 0)

    [self.collectionView reloadData];

    if (hadMessages)
        [self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:[newMessages count] inSection:0] atScrollPosition:UICollectionViewScrollPositionTop animated:NO];

到目前为止,这似乎有效。重新加载集合,在没有动画的情况下将之前的第一条消息滚动到顶部。

【讨论】:

【参考方案14】:

我设法编写了一个解决方案,适用于同时在顶部和底部插入单元格的情况。

    保存顶部可见单元格的位置。计算导航栏下方单元格的高度(顶视图。在我的例子中是 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;
    重新加载您的数据。
[self.collectionView reloadData];
    获取项目的新位置。获取该索引的属性。提取偏移量并更改collectionView的contentOffset。
// 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);

【讨论】:

【参考方案15】:
// 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)

【讨论】:

【参考方案16】:

我发现这五个步骤可以无缝地工作:

    为新单元格准备数据,并根据需要插入数据

    告诉UIView停止动画

    UIView.setAnimationsEnabled(false)
    

    实际插入那些单元格

    collectionView?.insertItems(at: indexPaths)
    

    滚动集合视图(UIScrollView 的子类)

    scrollView.contentOffset.y += CELL_HEIGHT * CGFloat(ITEM_COUNT)
    

    注意将 CELL_HEIGHT 替换为单元格的高度(这仅在单元格大小固定时才容易实现)。添加任何单元格到单元格的边距/插图很重要。

    记得告诉UIView重新开始动画:

    UIView.setAnimationsEnabled(true)
    

【讨论】:

【参考方案17】:

一些建议的方法对我来说取得了不同程度的成功。我最终使用了子类化的变体和prepareLayout 选项 Peter Stajger 将我的偏移校正放在finalizeCollectionViewUpdates 中。然而今天,当我查看一些额外的文档时,我发现了targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint),我认为这更像是此类更正的预期位置。所以这是我使用它的实现。请注意,我的实现是针对水平集合,但 cellsInsertingToTheLeft 可以轻松更新为 cellsInsertingAbove 并相应地纠正偏移量。

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
    

【讨论】:

【参考方案18】:

根据@Steven 的回答,我设法使插入单元格滚动到底部,没有任何闪烁(并使用自动单元格),在 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()

【讨论】:

【参考方案19】:

我使用了@James Martin 方法,但如果您使用coredataNSFetchedResultsController,正确的方法是将之前加载的消息的数量存储在_earlierMessagesLoaded 中并检查值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];

【讨论】:

【参考方案20】:
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;

【讨论】:

以上是关于UICollectionView 在保持位置上方插入单元格(如 Messages.app)的主要内容,如果未能解决你的问题,请参考以下文章

UICollectionView 保持水平列表滚动位置

在 UICollectionView 中重新加载数据后保持 collectionview 速度

将 UIRefreshControl 置于滚动 UICollectionView 上方的正确方法是啥?

UICollectionView-如何删除第一个单元格上方的间距?

UICollectionView 在顶部加载项目 - 加载更多选项

将按钮置于底部上方