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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/25548257/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-31 02:05:34  来源:igfitidea点击:

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

iosuicollectionview

提问by mrvn

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?

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

回答by James Martin

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

回答by Peter Stajger

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:

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

define variables

定义变量

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

overrideprepareLayout()and after calling super

覆盖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
}

回答by Sebastian

James Martin's fantastic version converted to Swift 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()
})

回答by Fogmeister

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.

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

I rotated the tableview 180 degrees.

我将 tableview 旋转了 180 度。

Then I rotated each tableview cell by 180 degrees also.

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

This meant that I could treat it as a standard top to bottom table but the bottom was treated like the top.

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

回答by mattr

Adding to Fogmeister's answer (with code), the cleanest approach is to invert (turn upside-down) the UICollectionViewso 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.

添加到 Fogmeister 的答案(使用代码),最简洁的方法是反转(颠倒),UICollectionView这样您就有一个粘在底部而不是顶部的滚动视图。UITableView正如 Fogmeister 指出的那样,这也适用于。

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

这也会产生倒置显示单元格的副作用,因此您也必须翻转它们。所以我们cell.transform = collectionView.transform像这样传递变换():

- (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 NSIndexPathparameters in delegates are reversed. So indexPath.row == 0is the row at on the bottom of the collectionViewwhere it is normally at the top.

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

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

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

Thought I would add some code context to Fogmeister's fantastic answer!

以为我会在 Fogmeister 的精彩答案中添加一些代码上下文!

回答by mriaz0011

Swift 3 version code:based on James Martin answer

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 i in 0..<amount {
        let index = 0 + i
        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()
    })

回答by xaphod

Here's a slightly tweaked version of Peter's solution (subclassing flow layout, no upside-down, lightweight approach). It's Swift 3. Note UIView.animatewith 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)

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

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: 
    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
            }
        }
    }
.item+twitterItems, section: 0) }) } if let insertionIndexPaths = insertionIndexPaths, insertionIndexPaths.count > 0 { self.collectionview.insertItems(at: insertionIndexPaths.map { return IndexPath.init(item:
@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];
        }
    }
}
.item+twitterItems, section: 0) }) } }) { (finished) in completionBlock?() }

Here's ContentSizePreservingFlowLayoutin its entirety:

下面是ContentSizePreservingFlowLayout其全部:

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

回答by Bryan Pratte

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

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

[self.collectionView reloadData];

回答by Jochen Sch?llig

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 3and absolutely usable with RxSwiftand RxDataSourcesbecause UI is completely separated from any logic or binding.

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

Three things were important for me:

对我来说,三件事很重要:

  1. 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 setContentOffsetinstead of scrollToItemAtIndexPath.
  2. If you do "Lazy Loading" with older messages, then the scroll view shouldn't change and stays exactly where it is.
  3. Add exceptions for the beginning. The collection view should behave "normal" till there are more messages than space on the screen.
  1. 如果有新消息,请向下滚动到它。此刻您在列表中的哪个位置并不重要。滚动是通过setContentOffset代替实现的scrollToItemAtIndexPath
  2. 如果您对旧消息进行“延迟加载”,则滚动视图不应更改并保持原样。
  3. 为开头添加例外。集合视图应该表现“正常”,直到屏幕上的消息多于空间。

My solution:https://gist.github.com/jochenschoellig/04ffb26d38ae305fa81aeb711d043068

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

回答by grigorievs

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)
  1. 保存顶部可见单元格的位置。计算导航栏下方的单元格的高度(顶视图。在我的情况下,它是 self.participantsView)
// 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);
  1. Reload your data.
  1. 重新加载您的数据。
##代码##
  1. Get the new position of the item. Get the attributes for that index. Extract the offset and change contentOffset of the collectionView.
  1. 获取项目的新位置。获取该索引的属性。提取collectionView的偏移量并改变contentOffset。
##代码##