Created
May 10, 2018 12:46
-
-
Save AshvinGudaliya/d562af60bab84984590e099896eea4ac 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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment