SectionIndexTitles for a UICollectionView

I had a similar requirement (for a horizontal collection view) and ended up building an index view subclass myself.

I plan to open-source it but that will likely have to wait until next month, so here's a stub to get you started:

YMCollectionIndexView.h

@interface YMCollectionIndexView : UIControl

- (id) initWithFrame:(CGRect)frame indexTitles:(NSArray *)indexTitles;

// Model
@property (strong, nonatomic) NSArray *indexTitles; // NSString
@property (readonly, nonatomic) NSUInteger currentIndex;
- (NSString *)currentIndexTitle;

@end

YMCollectionIndexView.m

#import "YMCollectionIndexView.h"

@interface YMCollectionIndexView ()
@property (readwrite, nonatomic) NSUInteger currentIndex;
@property (strong, nonatomic) NSArray *indexLabels;
@end

@implementation YMCollectionIndexView

- (id) initWithFrame:(CGRect)frame indexTitles:(NSArray *)indexTitles {
    self = [super initWithFrame:frame];
    if (self) {
        self.indexTitles = indexTitles;
        self.currentIndex = 0;
        // add pan recognizer
    }
    return self;
}

- (void)setIndexTitles:(NSArray *)indexTitles {
    if (_indexTitles == indexTitles) return;
    _indexTitles = indexTitles;
    [self.indexLabels makeObjectsPerformSelector:@selector(removeFromSuperview)];
    [self buildIndexLabels];
}

- (NSString *)currentIndexTitle {
    return self.indexTitles[self.currentIndex];
}

#pragma mark - Subviews

- (void) buildIndexLabels {
    CGFloat cumulativeItemWidth = 0.0; // or height in your (vertical) case
    for (NSString *indexTitle in self.indexTitles) {
            // build and add label
        // add tap recognizer
    }
    self.indexLabels = indexLabels;
}

#pragma mark - Gestures

- (void) handleTap:(UITapGestureRecognizer*)recognizer {
    NSString *indexTitle = ((UILabel *)recognizer.view).text;
    self.currentIndex = [self.indexTitles indexOfObject:indexTitle];
    [self sendActionsForControlEvents:UIControlEventTouchUpInside];
}

// similarly for pan recognizer

@end

In your view controller:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.collectionIndexView addTarget:self action:@selector(indexWasTapped:) forControlEvents:UIControlEventTouchUpInside];
    // similarly for pan recognizer
}

- (void)indexWasTapped:(id)sender {
    [self.collectionView scrollToIndexPath:...];
}

// similarly for pan recognizer

As of iOS 14 (which has severely enhanced UICollectionView, in particular for making UITableView obsolete), this works as expected by implementing the delegate method indexTitles.


The index titles do indeed work on iOS but it seems to only work on iOS 14+ when I tested.

func indexTitles(for collectionView: UICollectionView) -> [String]? {
    return Array(Set(objects.map{ String($0.name.prefix(1)) })).sorted(by: { $0 < $1 })
}

func collectionView(_ collectionView: UICollectionView, indexPathForIndexTitle title: String, at index: Int) -> IndexPath {
    guard let index = objects.firstIndex(where: { $0.name.prefix(1) == title }) else {
        return IndexPath(item: 0, section: 0)
    }
    return IndexPath(item: index, section: 0)
}