UIScrollView infinite scroll for dates
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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