Skip to content

Instantly share code, notes, and snippets.

@heydona
Created February 6, 2013 02:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save heydona/4719780 to your computer and use it in GitHub Desktop.
Save heydona/4719780 to your computer and use it in GitHub Desktop.
UIScrollView infinite scroll for dates
@interface MealPlanScrollView () {
NSMutableArray *_dayViews;
NSInteger _numberOfDays;
NSInteger _viewFirstDateNum;
NSInteger _viewLastDateNum;
NSInteger _todayDateNum;
NSInteger _maxNumberOfDays;
}
- (void)buildDayViews;
- (MealsDateView *)insertDayAtIndex:(NSInteger)index;
- (CGFloat)deleteDay:(MealsDateView *)day;
- (void)adjustDayOriginsBy:(CGFloat)distance startingAt:(NSInteger)start;
- (MealsDateView *)findViewForDateNum:(NSInteger)dateNum;
- (void)setTodayRow;
@end
@implementation MealPlanScrollView
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Base the number of days on the device size
// use a week either side to allow for smooth scrolling when moving by 7 days
_numberOfDays = roundf([UIScreen portraitHeight] / DEFAULT_ROW_HEIGHT) + 14;
_maxNumberOfDays = [NSDate maxNumberOfDates];
[self setContentSize:CGSizeMake(CGRectGetWidth(frame), 0)];
[self setDelegate:self];
[self setTodayRow];
_viewFirstDateNum = _todayDateNum - _numberOfDays/2;
_viewLastDateNum = _todayDateNum + _numberOfDays/2;
// Build the day views
_dayViews = [[NSMutableArray alloc] init];
[self buildDayViews];
[self setYear];
MealsDateView *firstVisible = _dayViews[(_numberOfDays/2)-1];
[self setContentOffset:CGPointMake(0, CGRectGetMinY([firstVisible frame]))];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
// Adjust the scroll offset if gone too far one way or the other
CGPoint currentOffset = [self contentOffset];
currentOffset.x = 0;
CGFloat contentHeight = [self contentSize].height;
CGFloat centerOffsetY = (contentHeight - CGRectGetHeight([self bounds])) / 2.0f;
CGFloat distanceFromCenter = currentOffset.y - centerOffsetY;
CGFloat buffer = DEFAULT_ROW_HEIGHT;
if (fabs(distanceFromCenter) > buffer) {
if (distanceFromCenter < 0) {
// move from bottom to top
while (fabs(distanceFromCenter) > buffer) {
if (_viewFirstDateNum == 0) {
break;
}
_viewFirstDateNum--;
_viewLastDateNum--;
[self deleteDay:[_dayViews lastObject]];
MealsDateView *firstDay = [self insertDayAtIndex:0];
CGFloat h = CGRectGetHeight([firstDay frame]);
currentOffset.y += h;
[self adjustDayOriginsBy:h startingAt:1];
distanceFromCenter += h;
if (distanceFromCenter >= 0) {
break;
}
}
} else {
// move from top to bottom
while (distanceFromCenter > buffer) {
if ((_viewLastDateNum-1) == _maxNumberOfDays) {
break;
}
_viewFirstDateNum++;
_viewLastDateNum++;
CGFloat h = [self deleteDay:_dayViews[0]];
[self insertDayAtIndex:[_dayViews count]];
[self adjustDayOriginsBy:-1*h startingAt:0];
currentOffset.y -= h;
distanceFromCenter -= h;
if (distanceFromCenter <= 0) {
break;
}
}
}
}
[self setContentOffset:currentOffset];
}
#pragma mark - Public
- (void)changeRowHeightOfDate:(NSDate *)date by:(CGFloat)delta {
NSInteger dateNum = [date daysSinceStart];
MealsDateView *day = [self findViewForDateNum:dateNum];
if (!day) {
return;
}
CGFloat oldHeight = CGRectGetHeight([day frame]);
// Adjust the size of the views
CGFloat newHeight = oldHeight + delta;
if (newHeight != oldHeight) {
CGFloat diff = newHeight - oldHeight;
CGSize s = [self contentSize];
s.height += diff;
[self setContentSize:s];
[self adjustDayOriginsBy:diff startingAt:(dateNum-_viewFirstDateNum+1)];
}
}
- (void)layoutMeals {
CGRect f = [self frame];
f.size.width = [UIScreen width];
f.size.height = [UIScreen height]-[UIScreen statusBarHeight]-f.origin.y;
[self setFrame:f];
CGFloat y = 0;
for (MealsDateView *day in _dayViews) {
CGRect df = [day frame];
if (CGRectGetMinX(df) != y) {
df.origin.y = y;
[day setFrame:df];
}
[day layoutMeals];
y += CGRectGetHeight([day frame]);
}
CGSize s = [self contentSize];
s.height = y;
[self setContentSize:s];
}
#pragma mark - Meal actions
- (void)scrollToDate:(NSDate *)date inRect:(CGRect)rect {
BOOL reload = NO;
NSInteger newDateNum = [date daysSinceStart];
if ((newDateNum <= _viewFirstDateNum) || (newDateNum >= _viewLastDateNum)) {
_viewFirstDateNum = newDateNum - _numberOfDays/2;
_viewLastDateNum = newDateNum + _numberOfDays/2;
if (_viewFirstDateNum < 0) {
_viewFirstDateNum = 0;
_viewLastDateNum = _viewFirstDateNum + _numberOfDays;
}
if (_viewLastDateNum > [NSDate maxNumberOfDates]) {
_viewLastDateNum = [NSDate maxNumberOfDates];
_viewFirstDateNum = _viewLastDateNum - _numberOfDays;
}
reload = YES;
}
if (reload) {
[self buildDayViews];
}
NSInteger newRow = newDateNum - _viewFirstDateNum;
if (newRow < 0) {
newRow = 0;
}
if (newRow >= [_dayViews count]) {
newRow = [_dayViews count] - 1;
}
MealsDateView *day = _dayViews[newRow];
CGRect f = [self frame];
CGRect df = [day frame];
CGPoint offset = [self contentOffset];
// rect is relative to the screen
offset.y = df.origin.y - (rect.origin.y - f.origin.y);
if (offset.y < 0) {
offset.y = 0;
}
[CATransaction begin];
[self setContentOffset:offset animated:!reload];
[CATransaction commit];
}
- (void)scrollByDistance:(CGFloat)distance {
CGPoint offset = [self contentOffset];
offset.y -= distance;
[UIView animateWithDuration:IOS_ANIMATION_DURATION
delay:0.0f
options:IOS_ANIMATION_CURVE
animations:^{
[self setContentOffset:offset animated:NO];
}
completion:^(BOOL finished) {
if (finished) {
}
}];
}
#pragma mark - Local
- (void)buildDayViews {
for (MealsDateView *day in _dayViews) {
[day removeFromSuperview];
}
[_dayViews removeAllObjects];
CGSize s = [self contentSize];
s.height = 0;
[self setContentSize:s];
for (int i=0; i<_numberOfDays; i++) {
[self insertDayAtIndex:i];
}
}
- (MealsDateView *)insertDayAtIndex:(NSInteger)index {
NSInteger dayNum = (_viewFirstDateNum + index);
NSDate *thisDate = [NSDate dateSinceStartForDayNumber:dayNum];
CGFloat y = 0;
if (index > 0) {
UIView *prevDay = _dayViews[(index-1)];
y = CGRectGetMaxY([prevDay frame]);
}
DateRowType rowType = DateRowTypeMiddle;
if (dayNum == 0) {
rowType = DateRowTypeFirst;
} else if (dayNum == _maxNumberOfDays) {
rowType = DateRowTypeLast;
}
MealsDateView *day = [[MealsDateView alloc] initWithDate:thisDate rowType:rowType];
CGRect f = [day frame];
f.origin.y = y;
[day setFrame:f];
[_dayViews insertObject:day atIndex:index];
[self insertSubview:day atIndex:0];
CGSize s = [self contentSize];
s.height += CGRectGetHeight([day frame]);
[self setContentSize:s];
return day;
}
- (CGFloat)deleteDay:(MealsDateView *)day {
CGRect f = [day frame];
CGSize s = [self contentSize];
s.height -= CGRectGetHeight(f);
[self setContentSize:s];
[day removeFromSuperview];
[_dayViews removeObject:day];
return CGRectGetHeight(f);
}
- (void)adjustDayOriginsBy:(CGFloat)distance startingAt:(NSInteger)start {
for (int i=start; i<[_dayViews count]; i++) {
MealsDateView *day = (MealsDateView *)_dayViews[i];
CGRect f = [day frame];
f.origin.y += distance;
[day setFrame:f];
}
}
- (MealsDateView *)findViewForDateNum:(NSInteger)dateNum {
if ((dateNum < _viewFirstDateNum) || (dateNum > _viewLastDateNum)) {
return nil;
}
NSInteger index = dateNum - _viewFirstDateNum;
if ((index >= 0) && (index < [_dayViews count])) {
return _dayViews[index];
} else {
return nil;
}
}
#pragma mark - Date/time methods
- (void)setTodayRow {
_todayDateNum = [[NSDate today] daysSinceStart];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment