diff --git a/Common/UI/MITPullToRefresh/MITPullToRefresh.h b/Common/UI/MITPullToRefresh/MITPullToRefresh.h new file mode 100644 index 000000000..8199088c5 --- /dev/null +++ b/Common/UI/MITPullToRefresh/MITPullToRefresh.h @@ -0,0 +1,18 @@ +#import + +typedef NS_ENUM(NSUInteger, MITPullToRefreshState) { + MITPullToRefreshStateStopped = 0, + MITPullToRefreshStateTriggered, + MITPullToRefreshStateLoading +}; + +@interface UIScrollView (MITPullToRefresh) + +- (void)mit_addPullToRefreshWithActionHandler:(void (^)(void))actionHandler; +- (void)mit_triggerPullToRefresh; +- (void)mit_stopAnimating; + +@property (nonatomic, assign) BOOL mit_showsPullToRefresh; +@property (nonatomic, readonly) MITPullToRefreshState mit_pullToRefreshState; + +@end diff --git a/Common/UI/MITPullToRefresh/MITPullToRefresh.m b/Common/UI/MITPullToRefresh/MITPullToRefresh.m new file mode 100644 index 000000000..e1098ca2f --- /dev/null +++ b/Common/UI/MITPullToRefresh/MITPullToRefresh.m @@ -0,0 +1,414 @@ +#import "MITPullToRefresh.h" +#import + +static CGFloat const MITPullToRefreshViewHeight = 70; +static CGFloat const MITPullToRefreshTriggerHeight = 70; + +static void * MITPullToRefreshScrollViewProperty_pullToRefreshView = &MITPullToRefreshScrollViewProperty_pullToRefreshView; + +#pragma mark MITPullToRefreshView "public" interface + +@interface MITPullToRefreshView : UIView + +@property (nonatomic, readonly) BOOL isActive; +@property (nonatomic, readonly) MITPullToRefreshState state; +@property (nonatomic, copy) void (^pullToRefreshActionHandler)(void); + +- (void)activateWithScrollView:(UIScrollView *)scrollView; +- (void)deactivate; + +- (void)startLoading; +- (void)stopAnimating; + +@end + +#pragma mark UIScrollView (MITPullToRefresh_Internal) + +@interface UIScrollView (MITPullToRefresh_Internal) + +@property (nonatomic, strong) MITPullToRefreshView *pullToRefreshView; + +@end + +#pragma mark UIScrollView (MITPullToRefresh) + +@implementation UIScrollView (MITPullToRefresh) + +- (void)mit_addPullToRefreshWithActionHandler:(void (^)(void))actionHandler +{ + if (!self.pullToRefreshView) { + MITPullToRefreshView *view = [[MITPullToRefreshView alloc] initWithFrame:CGRectMake(0, 0, self.bounds.size.width, MITPullToRefreshViewHeight)]; + self.pullToRefreshView = view; + [self addSubview:view]; + } + + self.pullToRefreshView.pullToRefreshActionHandler = actionHandler; + self.mit_showsPullToRefresh = YES; +} + +- (void)mit_triggerPullToRefresh +{ + [self.pullToRefreshView startLoading]; +} + +- (void)mit_stopAnimating +{ + [self.pullToRefreshView stopAnimating]; +} + +- (void)setPullToRefreshView:(MITPullToRefreshView *)pullToRefreshView +{ + [self willChangeValueForKey:@"pullToRefreshView"]; + objc_setAssociatedObject(self, MITPullToRefreshScrollViewProperty_pullToRefreshView, + pullToRefreshView, + OBJC_ASSOCIATION_ASSIGN); + [self didChangeValueForKey:@"pullToRefreshView"]; +} + +- (MITPullToRefreshView *)pullToRefreshView +{ + return objc_getAssociatedObject(self, MITPullToRefreshScrollViewProperty_pullToRefreshView); +} + +- (void)setMit_showsPullToRefresh:(BOOL)mit_showsPullToRefresh +{ + self.pullToRefreshView.hidden = !mit_showsPullToRefresh; + + if (mit_showsPullToRefresh) { + [self.pullToRefreshView activateWithScrollView:self]; + } else { + [self.pullToRefreshView deactivate]; + } +} + +- (BOOL)mit_showsPullToRefresh +{ + return self.pullToRefreshView.isActive; +} + +- (MITPullToRefreshState)mit_pullToRefreshState +{ + return self.pullToRefreshView.state; +} + +@end + +#pragma mark MITPullToRefreshView "private" interface + +static NSString * const StartingProgressViewAnimationGroupKey = @"StartingProgressViewAnimationGroupKey"; +static NSString * const StartingLoadingViewAnimationGroupKey = @"StartingLoadingViewAnimationGroupKey"; + +static NSString * const LoadingViewChoppyRotationKey = @"LoadingViewChoppyRotationKey"; + +static NSString * const EndingLoadingViewAnimationGroupKey = @"EndingLoadingViewAnimationGroupKey"; + +@interface MITPullToRefreshView () + +@property (nonatomic, assign) BOOL isActive; +@property (nonatomic, assign) MITPullToRefreshState state; + +@property (nonatomic, weak) UIScrollView *scrollView; +@property (nonatomic, assign) UIEdgeInsets unmodifiedInsets; +@property (nonatomic, assign) BOOL originalAlwaysBounceVertical; + +@property (nonatomic, strong) UIImageView *progressView; +@property (nonatomic, strong) CAShapeLayer *maskLayer; +@property (nonatomic, strong) UIImageView *loadingView; + +@end + +#pragma mark MITPullToRefreshView + +@implementation MITPullToRefreshView : UIView + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + + if (self) { + self.autoresizingMask = UIViewAutoresizingFlexibleWidth; + self.state = MITPullToRefreshStateStopped; + + CGRect wheelFrame = CGRectMake(0, 0, 28, 28); + + self.loadingView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"mit_ptrf_loading_wheel"]]; + self.loadingView.frame = wheelFrame; + self.loadingView.alpha = 0; + [self addSubview:self.loadingView]; + + self.progressView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"mit_ptrf_progress_wheel"]]; + self.progressView.frame = wheelFrame; + self.progressView.alpha = 0; + [self addSubview:self.progressView]; + + self.maskLayer = [CAShapeLayer layer]; + self.maskLayer.frame = self.progressView.frame; + } + + return self; +} + +- (void)layoutSubviews +{ + CGRect progressViewBounds = [self.progressView bounds]; + CGPoint origin = CGPointMake(ceilf((CGRectGetWidth(self.bounds) - CGRectGetWidth(progressViewBounds)) / 2), ceilf(( CGRectGetHeight(self.bounds) - CGRectGetHeight(progressViewBounds)) / 2)); + CGRect progressViewFrame = CGRectMake(origin.x, origin.y - 10, CGRectGetWidth(progressViewBounds), CGRectGetHeight(progressViewBounds)); + + self.progressView.frame = progressViewFrame; + self.maskLayer.frame = self.progressView.bounds; + self.loadingView.frame = self.progressView.frame; +} + +- (void)activateWithScrollView:(UIScrollView *)scrollView +{ + if (self.isActive) { + return; + } + + self.isActive = YES; + self.scrollView = scrollView; + self.unmodifiedInsets = scrollView.contentInset; + + self.originalAlwaysBounceVertical = scrollView.alwaysBounceVertical; + scrollView.alwaysBounceVertical = YES; + + [self observeScrollView:scrollView]; +} + +- (void)deactivate +{ + if (!self.isActive) { + return; + } + + self.isActive = NO; + [self unobserveScrollView:self.scrollView]; + self.scrollView.alwaysBounceVertical = self.originalAlwaysBounceVertical; + self.scrollView = nil; +} + +- (void)startLoading +{ + self.state = MITPullToRefreshStateLoading; + [self setScrollViewContentInsetForLoadingAnimated:YES]; + [self startAnimating]; + self.pullToRefreshActionHandler(); +} + +- (void)startAnimating +{ + CAMediaTimingFunction *rotationTiming = [CAMediaTimingFunction functionWithControlPoints:0.3 :0.4 :0.15 :1]; + + // Rotate and fade out progress view + CABasicAnimation *progressRotation = [CABasicAnimation animation]; + progressRotation.keyPath = @"transform.rotation.z"; + progressRotation.duration = 1.4; + progressRotation.fromValue = @0; + progressRotation.toValue = @(M_PI_2); + progressRotation.timingFunction = rotationTiming; + + CABasicAnimation *progressFadeOut = [CABasicAnimation animation]; + progressFadeOut.keyPath = @"opacity"; + progressFadeOut.duration = 1; + progressFadeOut.fromValue = @1; + progressFadeOut.toValue = @0; + + CAAnimationGroup *progressViewRotationGroup = [[CAAnimationGroup alloc] init]; + progressViewRotationGroup.animations = @[progressRotation, progressFadeOut]; + progressViewRotationGroup.duration = 0.7; + + // Rotate loading view so it is in sync with progress view as progress view disappears + CABasicAnimation *loadingRotation = [CABasicAnimation animation]; + loadingRotation.keyPath = @"transform.rotation.z"; + loadingRotation.duration = 1.4; + loadingRotation.fromValue = @(0); + loadingRotation.toValue = @(M_PI_2); + loadingRotation.timingFunction = rotationTiming; + + CAKeyframeAnimation *loadingChoppyRotationAnimation = [CAKeyframeAnimation animation]; + loadingChoppyRotationAnimation.keyPath = @"transform.rotation.z"; + loadingChoppyRotationAnimation.duration = 0.9; + double pi_6 = M_PI / 6.0; + loadingChoppyRotationAnimation.values = @[@(pi_6), @(2.0 * pi_6), @(3.0 * pi_6), @(4.0 * pi_6), @(5.0 * pi_6), @(M_PI), @(7.0 * pi_6), @(8.0 * pi_6), @(9.0 * pi_6), @(10.0 * pi_6), @(11.0 * pi_6), @(2.0 * M_PI)]; + loadingChoppyRotationAnimation.calculationMode = kCAAnimationDiscrete; + loadingChoppyRotationAnimation.repeatCount = HUGE_VALF; + loadingChoppyRotationAnimation.additive = YES; + + [self.progressView.layer addAnimation:progressViewRotationGroup forKey:StartingProgressViewAnimationGroupKey]; + + [self.loadingView.layer addAnimation:loadingRotation forKey:nil]; + [self.loadingView.layer addAnimation:loadingChoppyRotationAnimation forKey:LoadingViewChoppyRotationKey]; + + self.progressView.alpha = 0; + self.loadingView.alpha = 1; + self.loadingView.transform = CGAffineTransformMakeRotation(M_PI_2); +} + +- (void)stopAnimating +{ + self.state = MITPullToRefreshStateStopped; + [self resetScrollViewContentInsetAnimated:YES]; + + if ([self.progressView.layer animationForKey:StartingProgressViewAnimationGroupKey]) { + [self.progressView.layer removeAnimationForKey:StartingProgressViewAnimationGroupKey]; + } + + [self.loadingView.layer removeAnimationForKey:LoadingViewChoppyRotationKey]; + + CFTimeInterval endingAnimationDuration = 0.25; + + CABasicAnimation *loadingRotation = [CABasicAnimation animation]; + loadingRotation.keyPath = @"transform.rotation.z"; + loadingRotation.duration = endingAnimationDuration; + loadingRotation.fromValue = @0; + loadingRotation.toValue = @(M_PI_2); + loadingRotation.additive = YES; + + CABasicAnimation *loadingSize = [CABasicAnimation animation]; + loadingSize.keyPath = @"transform.scale"; + loadingSize.duration = endingAnimationDuration; + loadingSize.fromValue = @1; + loadingSize.toValue = @0.5; + + CABasicAnimation *loadingFadeOut = [CABasicAnimation animation]; + loadingFadeOut.keyPath = @"opacity"; + loadingFadeOut.duration = endingAnimationDuration; + loadingFadeOut.fromValue = @1; + loadingFadeOut.toValue = @0.1; + + CAAnimationGroup *endingAnimationGroup = [[CAAnimationGroup alloc] init]; + endingAnimationGroup.animations = @[loadingRotation, loadingSize, loadingFadeOut]; + endingAnimationGroup.duration = endingAnimationDuration; + + [self.loadingView.layer addAnimation:endingAnimationGroup forKey:nil]; + + self.loadingView.alpha = 0; +} + +#pragma mark KVO + +- (void)observeScrollView:(UIScrollView *)scrollView +{ + [scrollView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil]; + [scrollView addObserver:self forKeyPath:@"frame" options:NSKeyValueObservingOptionNew context:nil]; +} + +- (void)unobserveScrollView:(UIScrollView *)scrollView +{ + [scrollView removeObserver:self forKeyPath:@"contentOffset"]; + [scrollView removeObserver:self forKeyPath:@"frame"]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if ([keyPath isEqualToString:@"contentOffset"]) { + [self scrollViewDidScroll:[[change valueForKey:NSKeyValueChangeNewKey] CGPointValue]]; + } else if ([keyPath isEqualToString:@"frame"]) { + [self layoutSubviews]; + } +} + +- (void)scrollViewDidScroll:(CGPoint)contentOffset +{ + [self adjustFrameOffset]; + CGFloat pullingDownHeight = -1 * (contentOffset.y + self.unmodifiedInsets.top); + + switch (self.state) { + case MITPullToRefreshStateStopped: { + if (self.scrollView.isDragging && pullingDownHeight >= MITPullToRefreshTriggerHeight) { + self.state = MITPullToRefreshStateTriggered; + } + break; + } + case MITPullToRefreshStateTriggered: { + if (!self.scrollView.isDragging) { + [self startLoading]; + } else if (pullingDownHeight < MITPullToRefreshTriggerHeight) { + self.state = MITPullToRefreshStateStopped; + } + break; + } + case MITPullToRefreshStateLoading: { + [self setScrollViewContentInsetForLoadingAnimated:NO]; + break; + } + } + + if (pullingDownHeight > 0 && self.state != MITPullToRefreshStateLoading) { + [self updateViewForProgress:(pullingDownHeight * 1 / MITPullToRefreshTriggerHeight)]; + } +} + +- (void)updateViewForProgress:(CGFloat)progress +{ + progress = MAX(0, MIN(1.0, progress)); + CGFloat alphaThreshold = 0.25; + + self.progressView.alpha = MIN(1, (progress / alphaThreshold)); + + [CATransaction begin]; + [CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions]; + + // We want to always show the top line, and then unmask the lines fully, one at a time + // This code breaks non-alpha-changing section of the progress into 11ths (since there are 12 lines and the first line is always shown, even at 0%) + // and determines a number of lines to show by starting with 1 and going up to 12 chunks of pi/6 rad + CGFloat numberOfLines = (11 * MAX(0, (progress - alphaThreshold))) / (1 - alphaThreshold); + numberOfLines = floorf(numberOfLines) + 1; + CGFloat progressRadians = numberOfLines * (M_PI / 6); + + // Start position is straight upward, minus half the section of a line + // We are going to use clockwise calculation since that is the direction we want the mask to unfold in + CGFloat startRadians = -M_PI_2 - (M_PI / 12); + + CGPoint maskCenter = CGPointMake(ceilf(CGRectGetWidth(self.maskLayer.frame) / 2.0), ceilf(CGRectGetWidth(self.maskLayer.frame) / 2.0)); + CGFloat radius = ceilf(CGRectGetWidth(self.maskLayer.frame) / 2.0); + + UIBezierPath *progressMaskPath = [UIBezierPath bezierPath]; + [progressMaskPath addArcWithCenter:maskCenter radius:radius startAngle:startRadians endAngle:(startRadians + progressRadians) clockwise:YES]; + [progressMaskPath addLineToPoint:maskCenter]; + [progressMaskPath closePath]; + + self.maskLayer.path = progressMaskPath.CGPath; + + self.progressView.layer.mask = self.maskLayer; + + [CATransaction commit]; +} + +- (void)resetScrollViewContentInsetAnimated:(BOOL)animated +{ + UIEdgeInsets currentInsets = self.scrollView.contentInset; + currentInsets.top = self.unmodifiedInsets.top; + + if (animated) { + [UIView animateWithDuration:0.3 delay:0 options:(UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState) animations:^{ + self.scrollView.contentInset = currentInsets; + } completion:nil]; + } else { + self.scrollView.contentInset = currentInsets; + } +} + +- (void)setScrollViewContentInsetForLoadingAnimated:(BOOL)animated +{ + CGFloat offset = MAX(self.scrollView.contentOffset.y * -1, 0); + UIEdgeInsets currentInsets = self.scrollView.contentInset; + currentInsets.top = MIN(offset, self.unmodifiedInsets.top + self.bounds.size.height); + + if (animated) { + [UIView animateWithDuration:0.3 delay:0 options:(UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState) animations:^{ + self.scrollView.contentInset = currentInsets; + } completion:nil]; + } else { + self.scrollView.contentInset = currentInsets; + } +} + +- (void)adjustFrameOffset +{ + CGRect frame = self.frame; + frame.origin.y = self.scrollView.contentOffset.y; + self.frame = frame; +} + +@end diff --git a/Common/UI/MITPullToRefresh/mit_ptrf_loading_wheel@2x.png b/Common/UI/MITPullToRefresh/mit_ptrf_loading_wheel@2x.png new file mode 100644 index 000000000..a612868f4 Binary files /dev/null and b/Common/UI/MITPullToRefresh/mit_ptrf_loading_wheel@2x.png differ diff --git a/Common/UI/MITPullToRefresh/mit_ptrf_progress_wheel@2x.png b/Common/UI/MITPullToRefresh/mit_ptrf_progress_wheel@2x.png new file mode 100644 index 000000000..925c3dec4 Binary files /dev/null and b/Common/UI/MITPullToRefresh/mit_ptrf_progress_wheel@2x.png differ diff --git a/MIT Mobile.xcodeproj/project.pbxproj b/MIT Mobile.xcodeproj/project.pbxproj index 24f703599..5cff91650 100644 --- a/MIT Mobile.xcodeproj/project.pbxproj +++ b/MIT Mobile.xcodeproj/project.pbxproj @@ -561,6 +561,8 @@ A564FA9419F82FCA00967F08 /* MITLibrariesItemLoanFineCollectionCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = A564FA9219F82FCA00967F08 /* MITLibrariesItemLoanFineCollectionCell.xib */; }; A564FA9819F832BF00967F08 /* MITLibrariesItemHoldCollectionCell.m in Sources */ = {isa = PBXBuildFile; fileRef = A564FA9619F832BF00967F08 /* MITLibrariesItemHoldCollectionCell.m */; }; A564FA9919F832BF00967F08 /* MITLibrariesItemHoldCollectionCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = A564FA9719F832BF00967F08 /* MITLibrariesItemHoldCollectionCell.xib */; }; + A565A7141AD422470079EC6B /* mit_ptrf_loading_wheel@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = A565A7121AD422470079EC6B /* mit_ptrf_loading_wheel@2x.png */; }; + A565A7151AD422470079EC6B /* mit_ptrf_progress_wheel@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = A565A7131AD422470079EC6B /* mit_ptrf_progress_wheel@2x.png */; }; A569BE6119CB610E0014C29E /* MITDiningMenuComparisonViewController.xib in Sources */ = {isa = PBXBuildFile; fileRef = 2CE1C89B18C156B0001B8FEB /* MITDiningMenuComparisonViewController.xib */; }; A569BE6219CB610E0014C29E /* MITDiningHallMenuComparisonLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 483C86621725956A00A054F1 /* MITDiningHallMenuComparisonLayout.m */; }; A569BE6319CB610E0014C29E /* MITDiningHallMenuComparisonSectionHeaderView.m in Sources */ = {isa = PBXBuildFile; fileRef = 483C86651725D9C900A054F1 /* MITDiningHallMenuComparisonSectionHeaderView.m */; }; @@ -568,6 +570,7 @@ A569BE6519CB610E0014C29E /* MITDiningHallMenuComparisonNoMealsCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 48A4F408174D6EB20012F749 /* MITDiningHallMenuComparisonNoMealsCell.m */; }; A56A57581ABBD2D700C4918A /* MITShuttleStopsPageViewControllerDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = A56A57571ABBD2D700C4918A /* MITShuttleStopsPageViewControllerDataSource.m */; }; A580266C193E04B600BD0DDE /* MITShuttleStopNotificationManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A580266B193E04B600BD0DDE /* MITShuttleStopNotificationManager.m */; }; + A58D64651AC9DDEB00251247 /* MITPullToRefresh.m in Sources */ = {isa = PBXBuildFile; fileRef = A58D64641AC9DDEB00251247 /* MITPullToRefresh.m */; }; A5918CD21A3F57C700D3190F /* UIKit+MITShuttles.m in Sources */ = {isa = PBXBuildFile; fileRef = A5918CD11A3F57C700D3190F /* UIKit+MITShuttles.m */; }; A5918CD61A3F837400D3190F /* MITShuttlePredictionLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = A5918CD51A3F837400D3190F /* MITShuttlePredictionLoader.m */; }; A5960A121940B98D0028E86C /* MITShuttleMapBusAnnotationView.m in Sources */ = {isa = PBXBuildFile; fileRef = A5960A111940B98D0028E86C /* MITShuttleMapBusAnnotationView.m */; }; @@ -1681,10 +1684,14 @@ A564FA9519F832BF00967F08 /* MITLibrariesItemHoldCollectionCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MITLibrariesItemHoldCollectionCell.h; sourceTree = ""; }; A564FA9619F832BF00967F08 /* MITLibrariesItemHoldCollectionCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MITLibrariesItemHoldCollectionCell.m; sourceTree = ""; }; A564FA9719F832BF00967F08 /* MITLibrariesItemHoldCollectionCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MITLibrariesItemHoldCollectionCell.xib; sourceTree = ""; }; + A565A7121AD422470079EC6B /* mit_ptrf_loading_wheel@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "mit_ptrf_loading_wheel@2x.png"; sourceTree = ""; }; + A565A7131AD422470079EC6B /* mit_ptrf_progress_wheel@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "mit_ptrf_progress_wheel@2x.png"; sourceTree = ""; }; A56A57561ABBD2D700C4918A /* MITShuttleStopsPageViewControllerDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MITShuttleStopsPageViewControllerDataSource.h; sourceTree = ""; }; A56A57571ABBD2D700C4918A /* MITShuttleStopsPageViewControllerDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MITShuttleStopsPageViewControllerDataSource.m; sourceTree = ""; }; A580266A193E04B600BD0DDE /* MITShuttleStopNotificationManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MITShuttleStopNotificationManager.h; sourceTree = ""; }; A580266B193E04B600BD0DDE /* MITShuttleStopNotificationManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MITShuttleStopNotificationManager.m; sourceTree = ""; }; + A58D64631AC9DDEB00251247 /* MITPullToRefresh.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MITPullToRefresh.h; sourceTree = ""; }; + A58D64641AC9DDEB00251247 /* MITPullToRefresh.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MITPullToRefresh.m; sourceTree = ""; }; A5918CD01A3F57C700D3190F /* UIKit+MITShuttles.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIKit+MITShuttles.h"; sourceTree = ""; }; A5918CD11A3F57C700D3190F /* UIKit+MITShuttles.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIKit+MITShuttles.m"; sourceTree = ""; }; A5918CD41A3F837400D3190F /* MITShuttlePredictionLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MITShuttlePredictionLoader.h; sourceTree = ""; }; @@ -3664,6 +3671,7 @@ 806204B41A7AAF1A004BD08A /* UI */ = { isa = PBXGroup; children = ( + A58D645F1AC9DC0600251247 /* MITPullToRefresh */, 806204B51A7AAF1A004BD08A /* CenterText */, 806204BE1A7AAF1A004BD08A /* LoadingActivityView */, 8062057A1A7AAF7A004BD08A /* Maps */, @@ -4359,6 +4367,17 @@ name = Views; sourceTree = ""; }; + A58D645F1AC9DC0600251247 /* MITPullToRefresh */ = { + isa = PBXGroup; + children = ( + A565A7121AD422470079EC6B /* mit_ptrf_loading_wheel@2x.png */, + A565A7131AD422470079EC6B /* mit_ptrf_progress_wheel@2x.png */, + A58D64631AC9DDEB00251247 /* MITPullToRefresh.h */, + A58D64641AC9DDEB00251247 /* MITPullToRefresh.m */, + ); + path = MITPullToRefresh; + sourceTree = ""; + }; A59A259E19A1432B00AD7E3A /* Views */ = { isa = PBXGroup; children = ( @@ -4877,6 +4896,7 @@ 22E180A919DDE6770017AC9A /* MITLibrariesYourAccountViewController.xib in Resources */, 2258CBDB19CA1E450067E35C /* MITLibrariesHomeViewController.xib in Resources */, 5E70E39D192F87CA00165B41 /* MITShuttleMapViewController.xib in Resources */, + A565A7141AD422470079EC6B /* mit_ptrf_loading_wheel@2x.png in Resources */, 801062611A0BC7C8002C8A8B /* MITLibrariesFormSheetOptionsSelectionViewController.xib in Resources */, 2CDA8CFF18B7E359002A16C0 /* NewsStoryExternalNoImageTableCell.xib in Resources */, 2737F3511A333EF400E728C3 /* MITBatchScanningAlertView.xib in Resources */, @@ -4908,6 +4928,7 @@ 0B7602561A3A231E0061AC3B /* NewsStoryDekCollectionViewCell.xib in Resources */, 2217548C1A07D48A002AD787 /* MITToursInfoCollectionCell.xib in Resources */, A56390E719E07F67001DE763 /* MITLibrariesAvailabilityDetailCell.xib in Resources */, + A565A7151AD422470079EC6B /* mit_ptrf_progress_wheel@2x.png in Resources */, 2209CC3119929BE300E17100 /* MITEventsTableViewController.xib in Resources */, 80598DBB1A0A81460095CEAA /* MITLibrariesFormSheetCellSingleLineTextEntry.xib in Resources */, 0B76025A1A3A231E0061AC3B /* NewsStoryLoadMoreCollectionViewCell.xib in Resources */, @@ -5404,6 +5425,7 @@ 80C5D9E01A111A3E00E63FDD /* MITLibrariesFormSheetElementDepartment.m in Sources */, 48B8129A15E42B1700E791F9 /* LinksModule.m in Sources */, 8062066B1A814A4C004BD08A /* MITAccessibilityConstants.m in Sources */, + A58D64651AC9DDEB00251247 /* MITPullToRefresh.m in Sources */, 8062053C1A7AAF1A004BD08A /* Foundation+MITAdditions.m in Sources */, 2C18090B18F33B95008F9775 /* MITTouchstoneConstants.m in Sources */, 2C18090A18F33B95008F9775 /* MITTouchstoneNetworkIdentityProvider.m in Sources */, diff --git a/Modules/Dining/MITDiningHouseVenueDetailViewController.m b/Modules/Dining/MITDiningHouseVenueDetailViewController.m index 02630e764..2170eed73 100644 --- a/Modules/Dining/MITDiningHouseVenueDetailViewController.m +++ b/Modules/Dining/MITDiningHouseVenueDetailViewController.m @@ -275,61 +275,55 @@ - (void)previousMealPressed:(id)sender - (MITDiningHouseMealListViewController *)nextViewControllerForCurrentMeal:(MITDiningMeal *)meal andCurrentDay:(MITDiningHouseDay *)day { - MITDiningHouseMealListViewController *next = [[MITDiningHouseMealListViewController alloc] initWithNibName:nil bundle:nil]; - if (meal) { - if ([day.sortedMealsArray.lastObject isEqual:meal]) { - if ([self.houseVenue.mealsByDay.lastObject isEqual:day]) { - next = nil; - } else { - NSUInteger idx = [self.houseVenue.mealsByDay indexOfObject:day]; - next.day = self.houseVenue.mealsByDay[idx + 1]; - next.meal = next.day.sortedMealsArray.firstObject; - } - } else { - NSUInteger idx = [day.sortedMealsArray indexOfObject:meal]; + MITDiningHouseMealListViewController *next = nil; + + if (meal && ![day.sortedMealsArray.lastObject isEqual:meal]) { + NSUInteger idx = [day.sortedMealsArray indexOfObject:meal]; + + if (idx != NSNotFound && idx < day.sortedMealsArray.count) { + next = [[MITDiningHouseMealListViewController alloc] initWithNibName:nil bundle:nil]; next.meal = day.sortedMealsArray[idx + 1]; next.day = day; } - } else { - if ([self.houseVenue.mealsByDay.lastObject isEqual:day]) { - next = nil; - } else { - NSUInteger idx = [self.houseVenue.mealsByDay indexOfObject:day]; + } else if (![self.houseVenue.mealsByDay.lastObject isEqual:day]) { + NSUInteger idx = [self.houseVenue.mealsByDay indexOfObject:day]; + + if (idx != NSNotFound && idx < self.houseVenue.mealsByDay.count) { + next = [[MITDiningHouseMealListViewController alloc] initWithNibName:nil bundle:nil]; next.day = self.houseVenue.mealsByDay[idx + 1]; next.meal = next.day.sortedMealsArray.firstObject; } } + [next applyFilters:self.filters]; + return next; } - (MITDiningHouseMealListViewController *)previousViewControllerForCurrentMeal:(MITDiningMeal *)meal andCurrentDay:(MITDiningHouseDay *)day { - MITDiningHouseMealListViewController *previous = [[MITDiningHouseMealListViewController alloc] initWithNibName:nil bundle:nil]; - if (meal) { - if ([day.sortedMealsArray.firstObject isEqual:meal]) { - if ([self.houseVenue.mealsByDay.firstObject isEqual:day]) { - previous = nil; - } else { - NSUInteger idx = [self.houseVenue.mealsByDay indexOfObject:day]; - previous.day = self.houseVenue.mealsByDay[idx - 1]; - previous.meal = previous.day.sortedMealsArray.lastObject; - } - } else { - NSUInteger idx = [day.sortedMealsArray indexOfObject:meal]; + MITDiningHouseMealListViewController *previous = nil; + + if (meal && ![day.sortedMealsArray.firstObject isEqual:meal]) { + NSUInteger idx = [day.sortedMealsArray indexOfObject:meal]; + + if (idx != NSNotFound && idx != 0) { + previous = [[MITDiningHouseMealListViewController alloc] initWithNibName:nil bundle:nil]; previous.meal = day.sortedMealsArray[idx - 1]; previous.day = day; } - } else { - if ([self.houseVenue.mealsByDay.firstObject isEqual:day]) { - previous = nil; - } else { - NSUInteger idx = [self.houseVenue.mealsByDay indexOfObject:day]; + } else if (![self.houseVenue.mealsByDay.firstObject isEqual:day]) { + NSUInteger idx = [self.houseVenue.mealsByDay indexOfObject:day]; + + if (idx != NSNotFound && idx != 0) { + previous = [[MITDiningHouseMealListViewController alloc] initWithNibName:nil bundle:nil]; previous.day = self.houseVenue.mealsByDay[idx - 1]; previous.meal = previous.day.sortedMealsArray.lastObject; } } + [previous applyFilters:self.filters]; + return previous; } diff --git a/Modules/ShuttleTrack/MITShuttleRouteStopMapContainerViewController.m b/Modules/ShuttleTrack/MITShuttleRouteStopMapContainerViewController.m index 8e9e04009..d9864f327 100644 --- a/Modules/ShuttleTrack/MITShuttleRouteStopMapContainerViewController.m +++ b/Modules/ShuttleTrack/MITShuttleRouteStopMapContainerViewController.m @@ -9,6 +9,7 @@ #import "UIKit+MITAdditions.h" #import "MITShuttleStopsPageViewControllerDataSource.h" #import "MITShuttleStopViewController.h" +#import "MITPullToRefresh.h" typedef NS_ENUM(NSUInteger, MITShuttleRouteStopMapContainerState) { MITShuttleRouteStopMapContainerStateRoute = 0, @@ -101,6 +102,10 @@ - (void)viewDidLoad if (!self.isRotating) { [self configureLayoutForState:self.state animated:NO]; } + + [self.scrollView mit_addPullToRefreshWithActionHandler:^{ + [self.routeViewController refresh]; + }]; } - (void)viewWillAppear:(BOOL)animated @@ -225,6 +230,7 @@ - (void)setupRouteViewController { self.routeViewController = [[MITShuttleRouteViewController alloc] initWithRoute:self.route]; self.routeViewController.delegate = self; + self.routeViewController.shouldShowRefreshControl = NO; self.routeViewController.tableView.scrollsToTop = NO; self.routeViewController.view.translatesAutoresizingMaskIntoConstraints = NO; @@ -392,6 +398,11 @@ - (void)routeViewController:(MITShuttleRouteViewController *)routeViewController [self setState:MITShuttleRouteStopMapContainerStateStop animated:YES]; } +- (void)routeViewControllerDidFinishRefreshing:(MITShuttleRouteViewController *)routeViewController +{ + [self.scrollView mit_stopAnimating]; +} + #pragma mark - Map Tap Gesture Recognizer - (void)mapContainerViewTapped @@ -439,6 +450,8 @@ - (void)configureLayoutForRouteStateAnimated:(BOOL)animated [self setTitleForRoute:self.route]; [self setRouteViewHidden:NO]; + self.scrollView.mit_showsPullToRefresh = YES; + if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation)) { [self.scrollView removeConstraint:self.mapContainerViewPortraitHeightConstraint]; self.mapContainerViewPortraitHeightConstraint = [NSLayoutConstraint constraintWithItem:self.mapContainerView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:kMapContainerViewEmbeddedHeightPortrait]; @@ -496,6 +509,8 @@ - (void)configureLayoutForStopStateAnimated:(BOOL)animated [self setTitleForRoute:self.route stop:self.stop animated:animated]; [self setStopViewHidden:NO]; + self.scrollView.mit_showsPullToRefresh = NO; + if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation)) { [self.scrollView removeConstraint:self.mapContainerViewPortraitHeightConstraint]; self.mapContainerViewPortraitHeightConstraint = [NSLayoutConstraint constraintWithItem:self.mapContainerView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:kMapContainerViewEmbeddedHeightPortrait]; @@ -549,6 +564,8 @@ - (void)configureLayoutForMapStateAnimated:(BOOL)animated { self.routeViewController.shouldSuppressPredictionRefreshReloads = YES; + self.scrollView.mit_showsPullToRefresh = NO; + if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation)) { [self.scrollView removeConstraint:self.routeStopContainerViewHeightConstraint]; self.routeStopContainerViewHeightConstraint = [NSLayoutConstraint constraintWithItem:self.routeStopContainerView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:0]; diff --git a/Modules/ShuttleTrack/MITShuttleRouteViewController.h b/Modules/ShuttleTrack/MITShuttleRouteViewController.h index da2b0d901..692a98206 100644 --- a/Modules/ShuttleTrack/MITShuttleRouteViewController.h +++ b/Modules/ShuttleTrack/MITShuttleRouteViewController.h @@ -15,9 +15,13 @@ // Used by container view controller so that we prevent weird tableview ui behavior when we are hiding this below the full-screen map @property (nonatomic, assign) BOOL shouldSuppressPredictionRefreshReloads; +// Defaults to YES +@property (nonatomic, assign) BOOL shouldShowRefreshControl; + - (instancetype)initWithRoute:(MITShuttleRoute *)route; - (void)highlightStop:(MITShuttleStop *)stop; +- (void)refresh; - (CGFloat)targetTableViewHeight; @end @@ -28,5 +32,6 @@ @optional - (void)routeViewControllerDidSelectMapPlaceholderCell:(MITShuttleRouteViewController *)routeViewController; +- (void)routeViewControllerDidFinishRefreshing:(MITShuttleRouteViewController *)routeViewController; @end diff --git a/Modules/ShuttleTrack/MITShuttleRouteViewController.m b/Modules/ShuttleTrack/MITShuttleRouteViewController.m index d5e036618..40bcce244 100644 --- a/Modules/ShuttleTrack/MITShuttleRouteViewController.m +++ b/Modules/ShuttleTrack/MITShuttleRouteViewController.m @@ -25,9 +25,6 @@ @interface MITShuttleRouteViewController () @property (strong, nonatomic) MITShuttleRouteStatusCell *routeStatusCell; @property (strong, nonatomic) NSTimer *routeRefreshTimer; -@property (nonatomic) BOOL isUpdating; - - @end @implementation MITShuttleRouteViewController @@ -39,6 +36,7 @@ - (instancetype)initWithRoute:(MITShuttleRoute *)route self = [super initWithStyle:UITableViewStylePlain]; if (self) { _route = route; + _shouldShowRefreshControl = YES; } return self; } @@ -66,7 +64,6 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; [[MITShuttlePredictionLoader sharedLoader] addPredictionDependencyForRoute:self.route]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(predictionsWillUpdate) name:kMITShuttlePredictionLoaderWillUpdateNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(predictionsDidUpdate) name:kMITShuttlePredictionLoaderDidUpdateNotification object:nil]; } @@ -101,33 +98,61 @@ - (void)setupTableView [self.tableView registerNib:[UINib nibWithNibName:kMITShuttleStopCellNibName bundle:nil] forCellReuseIdentifier:kMITShuttleStopCellIdentifier]; self.tableView.separatorInset = UIEdgeInsetsMake(0, self.tableView.frame.size.width, 0, 0); - UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init]; - [refreshControl addTarget:self action:@selector(refreshControlActivated:) forControlEvents:UIControlEventValueChanged]; - self.refreshControl = refreshControl; + if (self.shouldShowRefreshControl) { + [self addRefreshControl]; + } } #pragma mark - Update Data -- (void)predictionsWillUpdate -{ - self.isUpdating = YES; -} - - (void)predictionsDidUpdate { - self.isUpdating = NO; if (!self.shouldSuppressPredictionRefreshReloads) { [self.tableView reloadDataAndMaintainSelection]; } } +- (void)refresh +{ + [[MITShuttleController sharedController] getPredictionsForRoute:self.route completion:^(NSArray *predictionLists, NSError *error) { + if ([self.delegate respondsToSelector:@selector(routeViewControllerDidFinishRefreshing:)]) { + [self.delegate routeViewControllerDidFinishRefreshing:self]; + } + [self predictionsDidUpdate]; + }]; +} + #pragma mark - Refresh Control +- (void)setShouldShowRefreshControl:(BOOL)shouldShowRefreshControl +{ + if (_shouldShowRefreshControl == shouldShowRefreshControl) { + return; + } + + _shouldShowRefreshControl = shouldShowRefreshControl; + + if (shouldShowRefreshControl) { + [self addRefreshControl]; + } else { + self.refreshControl = nil; + } +} + +- (void)addRefreshControl +{ + UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init]; + [refreshControl addTarget:self action:@selector(refreshControlActivated:) forControlEvents:UIControlEventValueChanged]; + self.refreshControl = refreshControl; +} + - (void)refreshControlActivated:(id)sender { - [self predictionsWillUpdate]; [[MITShuttleController sharedController] getPredictionsForRoute:self.route completion:^(NSArray *predictionLists, NSError *error) { [self.refreshControl endRefreshing]; + if ([self.delegate respondsToSelector:@selector(routeViewControllerDidFinishRefreshing:)]) { + [self.delegate routeViewControllerDidFinishRefreshing:self]; + } [self predictionsDidUpdate]; }]; }