8

I am implementing an infinite-scrolling calendar. My issue is that I would like to set the current month as the title in the navigation bar and it should update while scrolling - once you pass the section header view the title should update in the nav bar.

A possible solution would be to set the view title in the method called - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath so that, when I calculate a new section Header, it also updates the title. The problem with this is that the title changes when the new section is at the bottom of the page.

Currently displaying the Section Header as title, not optimal

Is there a way to know the "current section" of UICollectionView once the user has scrolled to it? Or can you think of a way to improve my current solution?

To help the readers of this post, I posted my own sample code for this question at this GitHub repo.

Jordan H
  • 52,571
  • 37
  • 201
  • 351
maggix
  • 3,268
  • 1
  • 22
  • 36

4 Answers4

2

I have been pondering an algorithm that would allow you to know when the user has scrolled past a section header in order to update the title, and after some experimentation I have figured out how to implement the desired behavior.

Essentially, every time the scroll position changes you need to know what section the user is on and update the title. You do this via scrollViewDidScroll on the UIScrollViewDelegate - remembering a collection view is a scroll view. Loop over all the headers and find the one that's closest to the current scroll position, without having a negative offset. To do that, I utilized a property that stores an array of each section header's position. When a header is created, I store its position in the array at the appropriate index. Once you've found the header that's closest to your scroll position (or the index location of said header), simply update the title in the navigation bar with the appropriate title.

In viewDidLoad, fill the array property with NSNull for each section you have:

self.sectionHeaderPositions = [[NSMutableArray alloc] init];
for (int x = 0; x < self.sectionTitles.count; x++) {
    [self.sectionHeaderPositions addObject:[NSNull null]];
}

In collectionView:viewForSupplementaryElementOfKind:atIndexPath:, update the array with the position of the created header view:

NSNumber *position = [NSNumber numberWithFloat:headerView.frame.origin.y + headerView.frame.size.height];
[self.sectionHeaderPositions replaceObjectAtIndex:indexPath.section withObject:position];

In scrollViewDidScroll:, perform the calculations to determine which title is appropriate to display for that scroll position:

CGFloat currentScrollPosition = self.collectionView.contentOffset.y + self.collectionView.contentInset.top;
CGFloat smallestPositiveHeaderDifference = CGFLOAT_MAX;
int indexOfClosestHeader = NSNotFound;

//find the closest header to current scroll position (excluding headers that haven't been reached yet)
int index = 0;
for (NSNumber *position in self.sectionHeaderPositions) {
    if (![position isEqual:[NSNull null]]) {
        CGFloat floatPosition = position.floatValue;
        CGFloat differenceBetweenScrollPositionAndHeaderPosition = currentScrollPosition - floatPosition;
        if (differenceBetweenScrollPositionAndHeaderPosition >= 0 && differenceBetweenScrollPositionAndHeaderPosition <= smallestPositiveHeaderDifference) {
            smallestPositiveHeaderDifference = differenceBetweenScrollPositionAndHeaderPosition;
            indexOfClosestHeader = index;
        }
    }
    index++;
}
if (indexOfClosestHeader != NSNotFound) {
    self.currentTitle.text = self.sectionTitles[indexOfClosestHeader];
} else {
    self.currentTitle.text = self.sectionTitles[0];
}

This will correctly update the title in the nav bar once the user scrolls past the header for a section. If they scroll back up it will update correctly as well. It also correctly sets the title when they haven't scrolled past the first section. It however doesn't handle rotation very well. It also won't work well if you have dynamic content, which may cause the stored positions of the header views to be incorrect. And if you support jumping to a specific section, the user jumps to a section whose previous section's section header hasn't been created yet, and that section isn't tall enough such that the section header is underneath the nav bar (the last section perhaps), the incorrect title will be displayed in the nav bar.

If anyone can improve upon this to make it more efficient or otherwise better please do and I'll update the answer accordingly.

Jordan H
  • 52,571
  • 37
  • 201
  • 351
0

change below line in method viewForSupplementaryElementOfKind :

self.title = [df stringFromDate:[self dateForFirstDayInSection:indexPath.section]];

to this:

    if(![[self dateForFirstDayInSection:indexPath.section-1] isKindOfClass:[NSNull class]]){
        self.title = [df stringFromDate:[self dateForFirstDayInSection:indexPath.section-1]];
}

Hope it will help you.

KDeogharkar
  • 10,939
  • 7
  • 51
  • 95
  • This won't work when the next header isn't visible on screen, and when you scroll up it changes the title before the previous section is visible. – Jordan H Feb 21 '15 at 19:12
0

Yes, the problem is that footer and header are not exists in visibleCells collection. There is other way to detect scroll for section header/footer. Just add a control there and find the rect for it. Like this:

func scrollViewDidScroll(scrollView: UIScrollView) {

    if(footerButton.tag == 301)
    {
        let frame : CGRect = footerButton.convertRect(footerButton.frame, fromView: self.view)
        //some process for frame
    }


}
nerowolfe
  • 4,787
  • 3
  • 20
  • 19
0

No solution here is fulfilling, so I came up with my own that I want to share, fully. If you use this, you have to make some pixel adjustments, though.

extension MyViewControllerVC: UIScrollViewDelegate {

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if scrollView == self.myCollectionView {
            let rect = CGRect(origin: self.myCollectionView.contentOffset, size: self.cvProductItems.bounds.size)
            let cellOffsetX: CGFloat = 35 // adjust this
            let cellOffsetAheadY: CGFloat = 45 // adjust this
            let cellOffsetBehindY: CGFloat = 30 // adjust this
            var point: CGPoint = CGPoint(x: rect.minX + cellOffsetX, y: rect.minY + cellOffsetAheadY) // position of cell that is ahead

            var indexPath = self.myCollectionView.indexPathForItem(at: point)
            if indexPath?.section != nil { // reached next section
                // do something with your section (indexPath!.section)
            } else {
                point = CGPoint(x: rect.minX + cellOffsetX, y: rect.minY - cellOffsetBehindY) // position of cell that is behind
                indexPath = self.myCollectionView.indexPathForItem(at: point)
                if indexPath?.section != nil { // reached previous section
                        // do something with your section (indexPath!.section)
                }
            }
        }
    }

}

UICollectionView inherits UIScrollView, so we can just do self.myCollectionView.delegate = self in viewDidLoad() and implement the UIScrollViewDelegate for it.

In the scrollViewDidScroll callback we will first get the point of a cell below, adjust cellOffsetX and cellOffsetAheadY properly, so your section will be selected when the cell hits that point. You can also modify the CGPoint to get a different point from the visible rect, i.e for x you can also use rect.midX / rect.maxX and any custom offset.

An indexPath will be returned from indexPathForItem(at: GCPoint) when you hit a the cell with those coordinates.

When you scroll up, you might want to look ahead, possibly ahead your UICollectionReusableView header and footer, for this I also check the point with negative Y offset set in cellOffsetBehindY. This has lower priority.

So, this example will get the next section once you pass the header and the previous section once a cell of the previous section is about to get into view. You have to adjust it to fit your needs and you should store the value somewhere and only do your thing when then current section changes, because this callback will be called on every frame while scrolling.

Martin Braun
  • 10,906
  • 9
  • 64
  • 105