Skip to content

Instantly share code, notes, and snippets.

@jochenschoellig
Created January 19, 2017 15:49
Show Gist options
  • Star 49 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
  • Save jochenschoellig/04ffb26d38ae305fa81aeb711d043068 to your computer and use it in GitHub Desktop.
Save jochenschoellig/04ffb26d38ae305fa81aeb711d043068 to your computer and use it in GitHub Desktop.
A subclass of UICollectionViewFlowLayout to get chat behavior without turning collection view upside-down. This layout is written in Swift 3 and absolutely usable with RxSwift and RxDataSources because UI is completely separated from any logic or binding.
import UIKit
class ChatCollectionViewFlowLayout: UICollectionViewFlowLayout {
private var topMostVisibleItem = Int.max
private var bottomMostVisibleItem = -Int.max
private var offset: CGFloat = 0.0
private var visibleAttributes: [UICollectionViewLayoutAttributes]?
private var isInsertingItemsToTop = false
private var isInsertingItemsToBottom = false
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// Reset each time all values to recalculate them
// ════════════════════════════════════════════════════════════
// Get layout attributes of all items
visibleAttributes = super.layoutAttributesForElements(in: rect)
// Erase offset
offset = 0.0
// Reset inserting flags
isInsertingItemsToTop = false
isInsertingItemsToBottom = false
return visibleAttributes
}
override func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) {
// Check where new items get inserted
// ════════════════════════════════════════════════════════════
// Get collection view and layout attributes as non-optional object
guard let collectionView = self.collectionView else { return }
guard let visibleAttributes = self.visibleAttributes else { return }
// Find top and bottom most visible item
// ────────────────────────────────────────────────────────────
bottomMostVisibleItem = -Int.max
topMostVisibleItem = Int.max
let container = CGRect(x: collectionView.contentOffset.x,
y: collectionView.contentOffset.y,
width: collectionView.frame.size.width,
height: (collectionView.frame.size.height - (collectionView.contentInset.top + collectionView.contentInset.bottom)))
for attributes in visibleAttributes {
// Check if cell frame is inside container frame
if attributes.frame.intersects(container) {
let item = attributes.indexPath.item
if item < topMostVisibleItem { topMostVisibleItem = item }
if item > bottomMostVisibleItem { bottomMostVisibleItem = item }
}
}
// Call super after first calculations
super.prepare(forCollectionViewUpdates: updateItems)
// Calculate offset of inserting items
// ────────────────────────────────────────────────────────────
var willInsertItemsToTop = false
var willInsertItemsToBottom = false
// Iterate over all new items and add their height if they go inserted
for updateItem in updateItems {
switch updateItem.updateAction {
case .insert:
if topMostVisibleItem + updateItems.count > updateItem.indexPathAfterUpdate!.item {
if let newAttributes = self.layoutAttributesForItem(at: updateItem.indexPathAfterUpdate!) {
offset += (newAttributes.size.height + self.minimumLineSpacing)
willInsertItemsToTop = true
}
} else if bottomMostVisibleItem <= updateItem.indexPathAfterUpdate!.item {
if let newAttributes = self.layoutAttributesForItem(at: updateItem.indexPathAfterUpdate!) {
offset += (newAttributes.size.height + self.minimumLineSpacing)
willInsertItemsToBottom = true
}
}
case.delete:
// TODO: Handle removal of items
break
default:
break
}
}
// Pass on information if items need more than one screen
// ────────────────────────────────────────────────────────────
// Just continue if one flag is set
if willInsertItemsToTop || willInsertItemsToBottom {
// Get heights without top and bottom
let collectionViewContentHeight = collectionView.contentSize.height
let collectionViewFrameHeight = collectionView.frame.size.height - (collectionView.contentInset.top + collectionView.contentInset.bottom)
// Continue only if the new content is higher then the frame
// If it is not the case the collection view can display all cells on one screen
if collectionViewContentHeight + offset > collectionViewFrameHeight {
if willInsertItemsToTop {
CATransaction.begin()
CATransaction.setDisableActions(true)
isInsertingItemsToTop = true
} else if willInsertItemsToBottom {
isInsertingItemsToBottom = true
}
}
}
}
override func finalizeCollectionViewUpdates() {
// Set final content offset with animation or not
// ════════════════════════════════════════════════════════════
// Get collection view as non-optional object
guard let collectionView = self.collectionView else { return }
if isInsertingItemsToTop {
// Calculate new content offset
let newContentOffset = CGPoint(x: collectionView.contentOffset.x,
y: collectionView.contentOffset.y + offset)
// Set new content offset without animation
collectionView.contentOffset = newContentOffset
// Commit/end transaction
CATransaction.commit()
} else if isInsertingItemsToBottom {
// Calculate new content offset
// Always scroll to bottom
let newContentOffset = CGPoint(x: collectionView.contentOffset.x,
y: collectionView.contentSize.height + offset - collectionView.frame.size.height + collectionView.contentInset.bottom)
// Set new content offset with animation
collectionView.setContentOffset(newContentOffset, animated: true)
}
}
}
@vadym-kozak
Copy link

vadym-kozak commented Aug 13, 2018

for updateItem in updateItems { switch updateItem.updateAction { case .insert: if topMostVisibleItem + updateItems.count > updateItem.indexPathAfterUpdate!.item { if let newAttributes = self.layoutAttributesForItem(at: updateItem.indexPathAfterUpdate!) {

  • should be replaced by

let insertItems = updateItems.filter({ (updateItem) -> Bool in return updateItem.updateAction == .insert }) // Iterate over all new items and add their height if they go inserted for updateItem in updateItems { switch updateItem.updateAction { case .insert: if topMostVisibleItem + insertItems.count > updateItem.indexPathAfterUpdate!.item { if let newAttributes = self.layoutAttributesForItem(at: updateItem.indexPathAfterUpdate!) {

let insertItems = updateItems.filter({ (updateItem) -> Bool in return updateItem.updateAction == .insert })

needed to prevent scroll to top when insertions and updates in a single batch available

@romaHerman
Copy link

Hi @jochenschoellig

Is it working with latest swift and RXDatasource ?

I'm trying to use your code with with RXDataSource and collection view for chat and unfortunately it doesn't work and still scrolls to top

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment