Skip to content

Instantly share code, notes, and snippets.

@kreeger
Created February 11, 2013 17:19
Show Gist options
  • Save kreeger/4755877 to your computer and use it in GitHub Desktop.
Save kreeger/4755877 to your computer and use it in GitHub Desktop.
An index-title-scrubber-bar, for use with a UICollectionView (or even a PSTCollectionView). Gives a collection view the index title bar that a UITableView gets for (almost) free. A huge thank you to @yang from http://stackoverflow.com/a/14443540/194869, which saved my bacon here.
/*
* BDKCollectionIndexView.h
*/
#import <UIKit/UIKit.h>
/** The direction in which the control is oriented. Assists in determining layout values.
*/
typedef enum {
BDKCollectionIndexViewDirectionVertical = 0,
BDKCollectionIndexViewDirectionHorizontal
} BDKCollectionIndexViewDirection;
/** An index-title-scrubber-bar, for use with a UICollectionView (or even a PSTCollectionView). Gives a collection
* view the index title bar that a UITableView gets for (almost) free. A huge thank you to
* @Yang from http://stackoverflow.com/a/14443540/194869, which saved my bacon here.
*/
@interface BDKCollectionIndexView : UIControl
/** A collection of string values that represent section index titles.
*/
@property (strong, nonatomic) NSArray *indexTitles;
/** Indicates the position of the last-selected index title. Should map directly to a table view / collection section.
*/
@property (readonly, nonatomic) NSUInteger currentIndex;
/** The number of points used for padding at the end caps of the view (assists in layout).
*/
@property (nonatomic) CGFloat endPadding;
/** The direction in which the control is oriented; this is automatically set based on the frame given.
*/
@property (readonly) BDKCollectionIndexViewDirection direction;
/** The index title at the index of `currentIndex`.
*/
@property (readonly) NSString *currentIndexTitle;
/** A class message to initialize and return an index view control, given a frame and a list of index titles.
* @param frame the frame to use when initializing the control.
* @param indexTitles the index titles to be rendered out in the control.
* @return an instance of the class.
*/
+ (id)indexViewWithFrame:(CGRect)frame indexTitles:(NSArray *)indexTitles;
/** A message to initialize and return an index view control, given a frame and a list of index titles.
* @param frame the frame to use when initializing the control.
* @param indexTitles the index titles to be rendered out in the control.
* @return an instance of the class.
*/
- (id)initWithFrame:(CGRect)frame indexTitles:(NSArray *)indexTitles;
@end
/*
* BDKCollectionIndexView.m
*/
#import "BDKCollectionIndexView.h"
#import <QuartzCore/QuartzCore.h>
@interface BDKCollectionIndexView ()
/** A component that shows up under the letters to indicate the view is handling a touch or a pan.
*/
@property (strong, nonatomic) UIView *touchStatusView;
/** The collection of label subviews that are displayed (one for each index title).
*/
@property (strong, nonatomic) NSArray *indexLabels;
/** A gesture recognizer that handles panning.
*/
@property (strong, nonatomic) UIPanGestureRecognizer *panner;
/** A gesture recognizer that handles tapping.
*/
@property (strong, nonatomic) UITapGestureRecognizer *tapper;
/** A gesture recognizer that handles panning.
*/
@property (readonly) CGFloat theDimension;
/** Handles events sent by the tap gesture recognizer.
* @param recognizer the sender of the event; usually a UIPanGestureRecognizer.
*/
- (void)handleTap:(UITapGestureRecognizer *)recognizer;
/** Handles events sent by the pan gesture recognizer.
* @param recognizer the sender of the event; usually a UIPanGestureRecognizer.
*/
- (void)handlePan:(UIPanGestureRecognizer *)recognizer;
/** Handles logic for determining which label is under a given touch point, and sets `currentIndex` accordingly.
* @param point the touch point.
*/
- (void)setNewIndexForPoint:(CGPoint)point;
/** Handles setting the alpha component level for the background color on the `touchStatusView`.
* @param flag if `YES`, the `touchStatusView` is set to be visible and dark-ish.
*/
- (void)setBackgroundVisibility:(BOOL)flag;
@end
@implementation BDKCollectionIndexView
@synthesize currentIndex = _currentIndex, direction = _direction, theDimension = _theDimension;
+ (id)indexViewWithFrame:(CGRect)frame indexTitles:(NSArray *)indexTitles {
return [[self alloc] initWithFrame:frame indexTitles:indexTitles];
}
- (id)initWithFrame:(CGRect)frame indexTitles:(NSArray *)indexTitles {
if (self = [super initWithFrame:frame]) {
if (CGRectGetWidth(frame) > CGRectGetHeight(frame))
_direction = BDKCollectionIndexViewDirectionHorizontal;
else _direction = BDKCollectionIndexViewDirectionVertical;
_currentIndex = 0;
_endPadding = 2;
_panner = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
[self addGestureRecognizer:_panner];
_tapper = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
[self addGestureRecognizer:_tapper];
[self addSubview:self.touchStatusView];
self.indexTitles = indexTitles;
}
return self;
}
- (void)layoutSubviews {
CGFloat maxLength = 0.0;
switch (_direction) {
case BDKCollectionIndexViewDirectionHorizontal:
_theDimension = CGRectGetHeight(self.frame);
maxLength = CGRectGetWidth(self.frame) - (self.endPadding * 2);
break;
case BDKCollectionIndexViewDirectionVertical:
_theDimension = CGRectGetWidth(self.frame);
maxLength = CGRectGetHeight(self.frame) - (self.endPadding * 2);
break;
}
self.touchStatusView.frame = CGRectInset(self.bounds, 2, 2);
self.touchStatusView.layer.cornerRadius = floorf(self.theDimension / 2.75);
CGFloat cumulativeLength = self.endPadding;
CGSize labelSize = CGSizeMake(self.theDimension, self.theDimension);
CGFloat otherDimension = floorf(maxLength / self.indexLabels.count);
for (UILabel *label in self.indexLabels) {
switch (self.direction) {
case BDKCollectionIndexViewDirectionHorizontal:
labelSize.width = otherDimension;
label.frame = (CGRect){ { cumulativeLength, 0 }, labelSize };
cumulativeLength += CGRectGetWidth(label.frame);
break;
case BDKCollectionIndexViewDirectionVertical:
labelSize.height = otherDimension;
label.frame = (CGRect){ { 0, cumulativeLength }, labelSize };
cumulativeLength += CGRectGetHeight(label.frame);
break;
}
}
}
#pragma mark - Properties
- (UIView *)touchStatusView {
if (_touchStatusView) return _touchStatusView;
_touchStatusView = [[UIView alloc] initWithFrame:CGRectInset(self.bounds, 2, 2)];
_touchStatusView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0];
_touchStatusView.layer.cornerRadius = self.theDimension / 2;
_touchStatusView.layer.masksToBounds = YES;
return _touchStatusView;
}
- (void)setIndexTitles:(NSArray *)indexTitles {
if (_indexTitles == indexTitles) return;
_indexTitles = indexTitles;
[self.indexLabels makeObjectsPerformSelector:@selector(removeFromSuperview)];
[self buildIndexLabels];
}
- (NSString *)currentIndexTitle {
return self.indexTitles[self.currentIndex];
}
- (void)setEndPadding:(CGFloat)endPadding {
if (_endPadding == endPadding) return;
_endPadding = endPadding;
[self.indexTitles makeObjectsPerformSelector:@selector(removeFromSuperview)];
[self buildIndexLabels];
}
#pragma mark - Subviews
- (void)buildIndexLabels {
NSMutableArray *workingLabels = [NSMutableArray arrayWithCapacity:self.indexTitles.count];
for (NSString *indexTitle in self.indexTitles) {
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
label.text = indexTitle;
label.font = [UIFont boldSystemFontOfSize:12];
label.backgroundColor = [UIColor clearColor];
label.textColor = [UIColor colorWithRed:0.415 green:0.451 blue:0.490 alpha:1.0];
label.textAlignment = NSTextAlignmentCenter;
[self addSubview:label];
[workingLabels addObject:label];
}
self.indexLabels = [NSArray arrayWithArray:workingLabels];
}
- (void)setNewIndexForPoint:(CGPoint)point {
for (UILabel *view in self.indexLabels) {
if (CGRectContainsPoint(view.frame, point)) {
NSUInteger newIndex = [self.indexTitles indexOfObject:view.text];
if (newIndex != _currentIndex) {
_currentIndex = newIndex;
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
}
}
}
- (void)setBackgroundVisibility:(BOOL)flag {
CGFloat alpha = flag ? 0.25 : 0;
self.touchStatusView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:alpha];
}
#pragma mark - Gestures
- (void)handleTap:(UITapGestureRecognizer *)recognizer {
[self setBackgroundVisibility:!(recognizer.state == UIGestureRecognizerStateEnded)];
[self setNewIndexForPoint:[recognizer locationInView:self]];
}
- (void)handlePan:(UIPanGestureRecognizer *)recognizer {
[self setBackgroundVisibility:!(recognizer.state == UIGestureRecognizerStateEnded)];
CGPoint translation = [recognizer locationInView:self];
[self setNewIndexForPoint:translation];
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment