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

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?

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

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

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:
define variables
private var isInsertingCellsToTop: Bool = false
private var contentSizeWhenInsertingToTop: CGSize?
override prepareLayout() and after calling 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
}

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.
I rotated the tableview 180 degrees.
Then I rotated each tableview cell by 180 degrees also.
This meant that I could treat it as a standard top to bottom table but the bottom was treated like the top.

Swift 3 version code: based on James Martin answer
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()
})

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

Adding to Fogmeister's answer (with code), the cleanest approach is to invert (turn upside-down) the UICollectionView so 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.
- (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:
- (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 NSIndexPath parameters in delegates are reversed. So indexPath.row == 0 is the row at on the bottom of the collectionView where it is normally at the top.
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
Thought I would add some code context to Fogmeister's fantastic answer!

This is what I learned from JSQMessagesViewController: How maintain scroll position?. Very simple, useful and NO flicker!
// 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)

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 :)
#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];
}
}
}

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 3 and absolutely usable with RxSwift and RxDataSources because UI is completely separated from any logic or binding.
Three things were important for me:
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 setContentOffset instead of scrollToItemAtIndexPath.
If you do "Lazy Loading" with older messages, then the scroll view shouldn't change and stays exactly where it is.
Add exceptions for the beginning. The collection view should behave "normal" till there are more messages than space on the screen.
My solution:
https://gist.github.com/jochenschoellig/04ffb26d38ae305fa81aeb711d043068

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.

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

if ([newMessages count] > 0)
{
[self.collectionView reloadData];
if (hadMessages)
[self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:[newMessages count] inSection:0] atScrollPosition:UICollectionViewScrollPositionTop animated:NO];
}
This seems to be working so far. Reload the collection, scroll the previously first message to the top without animation.

I managed to write a solution which works for cases when inserting cells at the top and bottom at the same time.
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;
Reload your data.
[self.collectionView reloadData];
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);

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

I found the five steps work seamlessly:
Prepare data for your new cells, and insert the data as appropriate
Tell UIView to stop animation
UIView.setAnimationsEnabled(false)
Actually insert those cells
collectionView?.insertItems(at: indexPaths)
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.
Remember to tell UIView to start animation again:
UIView.setAnimationsEnabled(true)

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

Based on #Steven answer, I managed to make insert cell with scroll to the bottom, without any flickering (and using auto cells), tested on 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()

I have used the #James Martin approach, but if you use coredata and NSFetchedResultsController the right approach is store the number of earlier messages loaded in _earlierMessagesLoaded and check the value in the 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];
}

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;

Related

Method scrollToItemAtIndexPath is not working on iOS 14

I am having an issue with scrollToItemAtIndexPath from iOS 14.
In the previous iOS versions when the user stopped dragging, the next cell was centered horizontally, now the method scrollToItemAtIndexPath is ignored, and it remains stuck in the first cell.
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
if( scrollView.tag == 1 ) {
if (UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPad){
*targetContentOffset = scrollView.contentOffset; // set acceleration to 0.0
float pageWidth = (float) (self.view.frame.size.width)-80;
int minSpace = 10;
int cellToSwipe = (scrollView.contentOffset.x)/(pageWidth + minSpace) + (velocity.x < 0 ? 0 : 1); // cell width + min spacing for lines
if (cellToSwipe < 0) {
cellToSwipe = 0;
} else if (cellToSwipe >= MIN(6, self.news.count )) {
cellToSwipe = (int) MIN(6, self.news.count);
}
[self.newsCollectionView scrollToItemAtIndexPath: [NSIndexPath indexPathForRow:cellToSwipe inSection:0]
atScrollPosition: UICollectionViewScrollPositionCenteredHorizontally
animated: YES];
}
}
}
You can use layoutAttributesForItem(at indexPath: IndexPath) of the UICollectionViewLayout to calculate a proper contentOffset
The fix could be like that:
extension UICollectionView {
func scrollTo(indexPath: IndexPath) {
let attributes = collectionViewLayout.layoutAttributesForItem(at: indexPath)!
setContentOffset(attributes.frame.origin, animated: true)
}
}
scrollToItemAtIndexPath still has some problems on iOS 14, you can use setContentOffsetinstead, and it works for me.
Objective-C version:
UICollectionViewLayoutAttributes *attributes = [collectionView layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:section]];
[collectionView setContentOffset:attributes.frame.origin animated:YES];
where collectionView is your UICollectionView object, index is the row of the UICollectionView to which you want to scroll to and section is the section of the UICollectionView to which the row belongs to.
_collectionView.pagingEnabled = NO;
[_collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:(UICollectionViewScrollPositionLeft) animated:NO];
_collectionView.pagingEnabled = YES;

Collection view header flickers when resizing at some values

I have a custom UICollectionView layout that resizes when the user scrolls. As the header shrinks at one point it begins to flicker.
I'm guessing the issue is that when the header shrinks the collection view thinks it's out of frame and perhaps dequeues it but then it calculates that it is in frame and re-queues it which might be what's causing the flicker.
class CustomLayout: UICollectionViewFlowLayout, UICollectionViewDelegateFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect) as! [UICollectionViewLayoutAttributes]
let offset = collectionView!.contentOffset ?? CGPoint.zero
let minY = -sectionInset.top
if (offset.y >= minY) {
let setOffset = fabs(170 - minY)
let extraOffset = fabs(offset.y - minY)
if offset.y <= 170 {
for attributes in layoutAttributes {
if let elementKind = attributes.representedElementKind {
if elementKind == UICollectionElementKindSectionHeader {
var frame = attributes.frame
frame.size.height = max(minY, headerReferenceSize.height - (extraOffset * 1.25))
frame.origin.y = frame.origin.y + (extraOffset * 1.25)
attributes.frame = frame
}
}
}
} else {
for attributes in layoutAttributes {
if let elementKind = attributes.representedElementKind {
if elementKind == UICollectionElementKindSectionHeader {
var frame = attributes.frame
frame.size.height = max(minY, headerReferenceSize.height - (setOffset * 1.25))
frame.origin.y = frame.origin.y + (setOffset * 1.25)
attributes.frame = frame
}
}
}
}
}
return layoutAttributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
}
Here is a gif showing the behavior. Notice how it starts out fine and begins to flicker. Also fast scrolling has an undesired effect.
Any suggestions?
I don't think your issue is related to the layout code. I copied and tried using your CustomLayout in a simple sample app with no obvious flickering issues.
Other things to try:
Make sure your collectionView(viewForSupplementaryElementOfKind:at:) function properly reuses the header view, using collectionView.dequeueReusableSupplementaryView(ofKind:withReuseIdentifier:for:)). Creating a new header view each time could cause substantial delays.
Do you have any complex drawing code in the cell itself?
Worst case, you could try profiling the app using Instruments Time Profiler to see what operations are taking up the most CPU cycles (assuming dropped frames are your issue).
Looks like that your collection view moves the header to the back.
Try insert this in code where you're changing frame of the header:
collectionView.bringSubview(toFront: elementKind)

Calculate height of UITableViewCell offscreen with size classes

I've been working on a set of classes that allow for UITableView's and their associated classes to uses auto layout and dynamic type. It's based around an answer on Stack Overflow that aims to add auto layout support.
So far, it works well, but I've run in to a couple of problems when using size classes. The first is directly related to the table calculating the height:
When I create a new UITableViewCell, but don't add it to any views, the size class is AnyxAny, so when I've got some subviews or constraints that change based on the size class, they're always working as if they're in an AnyxAny situation. So far, my very hacky solution is to create a new UIWindow that I add the cells to by:
UIWindow(frame: UIScreen.mainScreen().applicationFrame)
This functions correctly, but I've got a couple of issues with it:
I'm now creating an entire new UIWindow object, which seems inefficient
Each cell has to be added to the same window
When the screen rotates, the size class may change (i.e., iPhone 6 Plus), so I need to listen for changes to the application frame and update my window's frame (not implemented yet)
Is there an easy/more efficient way to ensure the UITableViewCell knows its size class, without having to create a new UIWindow, or do that more efficiently? Maybe I can add a
Most of the current code can be found via the GitHub page, but the most relevant methods are:
DynamicTableViewController
private var cachedClassesForCellReuseIdentifiers = [String : UITableViewCell.Type]()
private var cachedNibsForCellReuseIdentifiers = [String : UINib]()
private var offscreenCellRowsForReuseIdentifiers = [String : UITableViewCell]()
private var offScreenWindow: UIWindow = {
return UIWindow(frame: UIScreen.mainScreen().applicationFrame)
}()
override public func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
// This method is called with an NSMutableIndexPath, which is not compatible with an imutable NSIndexPath,
// so we create an imutable NSIndexPath to be passed to the following methods
let imutableIndexPath = NSIndexPath(forRow: indexPath.row, inSection: indexPath.section)
if let reuseIdentifier = self.cellReuseIdentifierForIndexPath(imutableIndexPath) {
if let cell = self.cellForReuseIdentifier(reuseIdentifier) {
self.configureCell(cell, forIndexPath: indexPath)
if let dynamicCell = cell as? DynamicTableViewCell {
let height = dynamicCell.heightInTableView(tableView)
return height
} else {
// Fallback for non-DynamicTableViewCell cells
let size = cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
let cellBoundsHeight = CGRectGetHeight(cell.bounds)
if size.height > 0 && size.height >= cellBoundsHeight {
// +1 for the cell separator
return size.height + 1
} else {
// In some situations (such as the content view not having any/enough constraints to get a height), the
// size from the systemLayoutSizeFittingSize: will be 0. However, because this can _sometimes_ be intended
// (e.g., when adding to a default style; see: DynamicSubtitleTableViewCell), we just return
// the height of the cell as-is. This may make some cells look wrong, but overall will also prevent 0 being returned,
// hopefully stopping some things from breaking.
return cellBoundsHeight + 1
}
}
}
}
return UITableViewAutomaticDimension
}
private func cellForReuseIdentifier(reuseIdentifier: String) -> UITableViewCell? {
if self.offscreenCellRowsForReuseIdentifiers[reuseIdentifier] == nil {
if let cellClass = self.cachedClassesForCellReuseIdentifiers[reuseIdentifier] {
let cell = cellClass()
self.offScreenWindow.addSubview(cell)
self.offscreenCellRowsForReuseIdentifiers[reuseIdentifier] = cell
} else if let cellNib = self.cachedNibsForCellReuseIdentifiers[reuseIdentifier] {
if let cell = cellNib.instantiateWithOwner(nil, options: nil).first as? UITableViewCell {
self.offScreenWindow.addSubview(cell)
self.offscreenCellRowsForReuseIdentifiers[reuseIdentifier] = cell
}
}
}
return self.offscreenCellRowsForReuseIdentifiers[reuseIdentifier]
}
DynamicTableViewCell
public func heightInTableView(tableView: UITableView) -> CGFloat {
var height: CGFloat!
if self.calculateHeight {
self.setNeedsUpdateConstraints()
self.updateConstraintsIfNeeded()
self.bounds = CGRectMake(0, 0, CGRectGetWidth(tableView.bounds), CGRectGetHeight(self.bounds))
self.setNeedsLayout()
self.layoutIfNeeded()
let size = self.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
let boundsHeight = CGRectGetHeight(self.bounds)
if size.height > 0 && size.height >= boundsHeight {
// +1 for the cell separator
height = size.height + 1
} else {
// In some situations (such as the content view not having any/enough constraints to get a height), the
// size from the systemLayoutSizeFittingSize: will be 0. However, because this can _sometimes_ be intended
// (e.g., when adding to a default style; see: DynamicSubtitleTableViewCell), we just return
// the height of the cell as-is. This may make some cells look wrong, but overall will also prevent 0 being returned,
// hopefully stopping some things from breaking.
height = boundsHeight + 1
}
} else {
height = self.cellHeight
}
if height < self.minimumHeight && self.minimumHeight != nil {
return self.minimumHeight!
} else {
return height
}
}
This solutions works for iOS 7 and 8, and so should any future solutions. This restriction has also removed the use of UITraitCollection, so I've not gone down that route (I'm not even sure it'd help)
With IOS 8 and Autolayout You can set an estimated row height and set constraints for inner views with content view so it will scale according to the content in it. Here's my answer Which is the best approach among Autolayout or calculating the height using NSAttributedString, to implement dynamic height of an UITableViewCell?
Update: for IOS7 you need to calculate height when it is about to arrive you can do it in
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath Delegate method
#define HEIGHT_FOR_ONE_LINE 15; //define it according to your fonts
#define HEIGHT_FOR_SINGLE_ROWCELL 32;
#pragma mark - Table view data source
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *textToView= [self.yourDataSourceArray objectAtIndex:indexPath.row];
CGFloat screenWidth=[[UIScreen mainScreen]bounds].size.width;
CGFloat characterPerPoint= 10;
CGFloat widthTaken= characterPerPoint * textToView.length;
NSInteger numberOfLines= ceil(widthTaken/screenWidth);
CGFloat height=numberOfLines* HEIGHT_FOR_ONE_LINE;
height = height + HEIGHT_FOR_SINGLE_ROWCELL;
return height;
}

UICollectionview scroll horizontal and vertical

I have to build a UICollectionView scrollable horizontal and vertical, I know that the grid layout scrolls along one axis only, either horizontally or vertically, so I have read some posts and I have tried different solutions but the most simple is to put the UICollectionview inside a UIScrollView. In this way the CollectionView scroll vertically and the UIScrollView horizontally.
The problem is that the vertical scroll is difficult, not fluid and often is stop until you tap again and drag again.
Can you suggest a solution? Thanks
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
UIScrollView *backgroundScroll = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height)];
backgroundScroll.scrollEnabled = YES;
[self.view addSubview:backgroundScroll];
_collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(10, 15, 1020, [UIScreen mainScreen].bounds.size.height - 35) collectionViewLayout:layout];
[backgroundScroll addSubview:_collectionView];
_collectionView.contentInset = UIEdgeInsetsMake(0, 0, 50, 0);
_collectionView.scrollEnabled = YES;
And I have implemented the method:
- (void)viewDidLayoutSubviews {
backgroundScroll.contentSize = self.collectionView.frame.size;
}
The way to do this is to create a custom UICollectionViewLayout subclass.
I had to do this recently.
Let me go get the files... One sec...
First of all, you can't use a subclass of UICollectionViewFlowLayout easily for this. Flow layout is designed to fit the content in one direction and scroll in the other direction. This isn't what you want.
It isn't very difficult though to create a custom layout to do this for you.
Header File
#interface GridCollectionViewLayout : UICollectionViewLayout
// properties to configure the size and spacing of the grid
#property (nonatomic) CGSize itemSize;
#property (nonatomic) CGFloat itemSpacing;
// this method was used because I was switching between layouts
- (void)configureCollectionViewForLayout:(UICollectionView *)collectionView;
#end
Implementation
#import "GridCollectionViewLayout.h"
#interface GridCollectionViewLayout ()
#property (nonatomic, strong) NSDictionary *layoutInfo;
#end
#implementation GridCollectionViewLayout
Create inits for code and interface builder...
- (id)init
{
self = [super init];
if (self) {
[self setup];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
if (self) {
[self setup];
}
return self;
}
Setup defaults property values...
- (void)setup
{
self.itemSize = CGSizeMake(50.0, 50.0);
self.itemSpacing = 10.0;
}
This was used because I was changing between different layouts but it shows what is needed to set the layout..
- (void)configureCollectionViewForLayout:(UICollectionView *)collectionView
{
collectionView.alwaysBounceHorizontal = YES;
[collectionView setCollectionViewLayout:self animated:NO];
}
Required method. This iterates the items and creates frames CGRect for each one. Saving them into a dictionary.
- (void)prepareLayout
{
NSMutableDictionary *cellLayoutInfo = [NSMutableDictionary dictionary];
NSInteger sectionCount = [self.collectionView numberOfSections];
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0];
for (NSInteger section = 0; section < sectionCount; section++) {
NSInteger itemCount = [self.collectionView numberOfItemsInSection:section];
for (NSInteger item = 0; item < itemCount; item++) {
indexPath = [NSIndexPath indexPathForItem:item inSection:section];
UICollectionViewLayoutAttributes *itemAttributes =
[UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
itemAttributes.frame = [self frameForAssessmentAtIndexPath:indexPath];
cellLayoutInfo[indexPath] = itemAttributes;
}
}
self.layoutInfo = cellLayoutInfo;
}
This is a convenience method for quickly getting a frame at a given index.
- (CGRect)frameForIndexPath:(NSIndexPath *)indexPath
{
NSInteger column = indexPath.section;
NSInteger row = indexPath.item;
CGFloat originX = column * (self.itemSize.width + self.itemSpacing);
CGFloat originY = row * (self.itemSize.height + self.itemSpacing);
return CGRectMake(originX, originY, self.itemSize.width, self.itemSize.height);
}
Required method to calculate the content size. This just multiplies the number of sections or items by the size and spacing properties. This is what allows scrolling in both directions because the content size can be bigger than the collection view's width AND height.
- (CGSize)collectionViewContentSize
{
NSInteger sectionCount = [self.collectionView numberOfSections];
if (sectionCount == 0) {
return CGSizeZero;
}
NSInteger itemCount = [self.collectionView numberOfItemsInSection:0];
CGFloat width = (self.itemSize.width + self.itemSpacing) * sectionCount - self.itemSpacing;
CGFloat height = (self.itemSize.height + self.itemSpacing) * itemCount - self.itemSpacing;
return CGSizeMake(width, height);
}
Required methods. These tell the collection view where each item needs to be placed.
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
return self.layoutInfo[indexPath];
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray *allAttributes = [NSMutableArray array];
[self.layoutInfo enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath, UICollectionViewLayoutAttributes *attributes, BOOL *stop) {
if (CGRectIntersectsRect(attributes.frame, rect)) {
[allAttributes addObject:attributes];
}
}];
return allAttributes;
}
#end
Of course, the layout in this case is specific to my individual problem.
The layout worked by having each section be a column and the items in each section were the rows. So something like this...
xy = item y in section x
00 10 20 30 ...
01 11 21 31 ...
02 12 22 32 ...
. . . .
. . . .
. . . .
Obviously there can be an unlimited number of sections or items in sections so I had to have scrolling in both directions.
Once you have created your layout class you just need to set it as the layout for your collection view. You can do this in code collectionView.collectionViewLayout = myLayout or you can do it in Interface Builder with the "layout" property on the collection view.
I'd like to introduce a different approach to creating a UICollectionView that scrolls in one direction while displaying cells containing a CollectionView that scrolls in the opposite direction. By implementing this collection view, setting the scrollDirection on the UICollectionViewFlowLayout instance used for the collection view in question this solution provides a seamless response to the user's interaction.
The solution subclasses the UICollectionView, and adds a delay gesture recognizer that intercepts the user's touches, delays them for a split seconds to figure out which direction the user is intending to scroll, then cancelling panningRecognizer on the collection view that don't scroll in that specific direction.
import Foundation
import UIKit
class UIDirectionAbidingCollectionView : UICollectionView {
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout)
setupDelayRecognizer()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupDelayRecognizer()
}
func setupDelayRecognizer() {
addGestureRecognizer(delayPanGestureRecognizer)
// Delay the touches on the default recognizer on the collection view
panGestureRecognizer.delaysTouchesBegan = true
}
// This gesture recognizer controls the response to the user's touches
// by cancelling by failing panGesture recognizer on the collection view
// that scrolls in the opposite direction.
lazy var delayPanGestureRecognizer: UIPanGestureRecognizer = {
var recognizer = UIPanGestureRecognizer()
recognizer.delegate = self
return recognizer
}()
}
extension UIDirectionAbidingCollectionView: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
// Ensure that the delay recognizer needs to fails for the
// default panning recognizers to receives the touches
if (gestureRecognizer == delayPanGestureRecognizer &&
otherGestureRecognizer == panGestureRecognizer)
{
return true
}
return false
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
// If the recognizer in question is our delay recognizer
// lets check to see if it should begin receiving touches
if gestureRecognizer == delayPanGestureRecognizer {
// First retrieve the direction our flowlayout intends to scroll
if let flowLayout = self.collectionViewLayout as? UICollectionViewFlowLayout {
let scrollDirection = flowLayout.scrollDirection
// Retrieve the translation of the delayPanningRecognizer
let translation = delayPanGestureRecognizer.translation(in: self)
// Calculate the magnitude of change for the y and x axis
let xTransaltionValue = (translation.x * translation.x)
let yTransaltionValue = (translation.y * translation.y)
if scrollDirection == .vertical && xTransaltionValue > yTransaltionValue {
// If the scroll direction of the flowlayout is vertical,
// and the magnitude in the horizontal direction
// is greater than the horizontal, begin receiving touches.
// Since the delay recognizer doesn't fail, the vertical
// panning recognizer will fail to start on the collection view
return true
}
else if scrollDirection == .horizontal && xTransaltionValue < yTransaltionValue {
// If the scroll direction of the flowlayout is horizontal,
// and the magnitude in the vertical direction
// is greater than the horizontal, begin receiving touches.
// Since the delay recognizer doesn't fail, the horizontal
// panning recognizer will fail to start on the collection view
return true
}
else {
// Fail the delay recognizer, and allows the collection
// view to continue as usual
return false
}
}
}
return true
}
}

UITableView with one visible cell: determine which is most visible

Given a UITableView with a single visible cell at any given time, how can I determine which cell is most in view while the table view is being scrolled?
I know I can get an array of visible cells by doing this:
NSArray *paths = [tableView indexPathsForVisibleRows];
And then get the last cell (or first, or whatever) by doing:
UITableViewCell* cell = (UITableViewCell*)[tableView cellForRowAtIndexPath:[paths lastObject]];
But how to I compare all the visible cells and determine which of them is most in view?
The following logic would get you the most visible cell at the end of the scroll:
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
CGRect visibleRect = (CGRect){.origin = self.tableView.contentOffset, .size = self.tableView.bounds.size};
CGPoint visiblePoint = CGPointMake(CGRectGetMidX(visibleRect), CGRectGetMidY(visibleRect));
NSIndexPath *visibleIndexPath = [self.tableView indexPathForRowAtPoint:visiblePoint];
}
The algorithm is different depending on how many paths you get back:
If there is only one path, that's the most visible cell right there
If there are three or more paths, any of the cells in the middle (i.e. all cells except the first and the last ones) are equally visible
If there are exactly two cells, find the position of the line that separates the two in their parent view*, and compute two distances - top-to-middle and middle-to-bottom. If top-to-middle is greater, then the top cell is most visible. If middle-to-bottom is greater, then the second cell is more visible. Otherwise, the two cells are equally visible.
* Midpoint position is the bottom of the second cell. Top and bottom positions are the top and bottom of the table view.
Swift solution based on #Sebyddd's answer:
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
scrollToMostVisibleCell()
}
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate{
scrollToMostVisibleCell()
}
}
func scrollToMostVisibleCell(){
let visibleRect = CGRect(origin: tableView.contentOffset, size: tableView.bounds.size)
let visiblePoint = CGPoint(x: CGRectGetMidX(visibleRect), y: CGRectGetMidY(visibleRect))
let visibleIndexPath: NSIndexPath = tableView.indexPathForRowAtPoint(visiblePoint)!
tableView.scrollToRowAtIndexPath(visibleIndexPath, atScrollPosition: .Top, animated: true)
}
You can use the table view's rectForRowAtIndexPath: to get the frame of each visible cell, then offset them (with CGRectOffset) by -contentOffset.y to account for scrolling, then intersect them with the table view's bounds to find out how much each cell is visible inside the table view.
The below logic will give you the UITableViewCell which is most visible or closet to center in UITableView every time as soon as user stops scrolling. Hope this logic would help somebody.
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
if (!decelerate)
{
if (isScrollingStart)
{
isScrollingStart=NO;
isScrollingEnd=YES;
[self scrollingStopped];
}
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
if (isScrollingStart)
{
isScrollingStart=NO;
isScrollingEnd=YES;
[self scrollingStopped];
}
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
isScrollingStart=YES;
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
isScrollingStart=YES;
}
-(void)scrollingStopped
{
NSMutableArray* arrVideoCells=[NSMutableArray array];
NSLog(#"Scrolling stopped");
NSArray* arrVisibleCells=[self.tableTimeline visibleCells];
for (TimeLineCell* cell in arrVisibleCells)
{
if ([cell isKindOfClass:[TimeLineCellMediaVideo class]])
{
[arrVideoCells addObject:cell];
}
}
TimeLineCellMediaVideo* videoCell=[self getCellNearCenterOfScreen:arrVideoCells];
}
-(TimeLineCellMediaVideo*)getCellNearCenterOfScreen:(NSMutableArray*)arrCells
{
TimeLineCellMediaVideo* closetCellToCenter;
CGRect filterCGRect;
for (TimeLineCellMediaVideo* videoCell in arrCells)
{
if (arrCells.count==1)
closetCellToCenter= videoCell;
NSIndexPath* cellIndexPath=[self.tableTimeline indexPathForCell:videoCell];
CGRect rect = [self.tableTimeline convertRect:[self.tableTimeline rectForRowAtIndexPath:cellIndexPath] toView:[self.tableTimeline superview]];
if (closetCellToCenter)
{
CGRect intersect = CGRectIntersection(self.tableTimeline.frame, filterCGRect);
float visibleHeightFilterCell = CGRectGetHeight(intersect);
intersect = CGRectIntersection(self.tableTimeline.frame, rect);
float visibleHeightCurrentCell = CGRectGetHeight(intersect);
if (visibleHeightCurrentCell>visibleHeightFilterCell)
{
filterCGRect=rect;
closetCellToCenter= videoCell;
}
}
else
{
closetCellToCenter=videoCell;
filterCGRect=rect;
}
}
return closetCellToCenter;
}
I did the following to find indexPath for most visible cell and it is working correctly.
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
guard let tableView = scrollView as? UITableView else {
return
}
let visibleHeights = tableView.visibleCells.compactMap { cell -> (indexPath: IndexPath, visibleHeight: CGFloat)? in
guard let indexPath = tableView.indexPath(for: cell) else {
return nil
}
let cellRect = tableView.rectForRow(at: indexPath)
let superView = tableView.superview
let convertedRect = tableView.convert(cellRect, to: superView)
let intersection = tableView.frame.intersection(convertedRect)
let visibleHeight = intersection.height
return (indexPath, visibleHeight)
}
guard let maxVisibleIndexPath = visibleHeights.max(by: { $0.visibleHeight < $1.visibleHeight })?.indexPath else {
return
}
print("maxVisibleIndexPath: \(maxVisibleIndexPath)")
}

Resources