From f518eae17ad624773187fbf4b90ced464d0102c4 Mon Sep 17 00:00:00 2001 From: Ross LeBeau Date: Mon, 6 Apr 2015 22:19:54 -0400 Subject: [PATCH 01/10] Added checks for dining hall meal indexes when swiping meals to combat weird data issue causing out-of-bounds crash --- .../MITDiningHouseVenueDetailViewController.m | 62 +++++++++---------- 1 file changed, 28 insertions(+), 34 deletions(-) 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; } From 4bf35e9cb9b6886644e2484456b3c6346a08c8ee Mon Sep 17 00:00:00 2001 From: Ross LeBeau Date: Mon, 6 Apr 2015 16:30:28 -0400 Subject: [PATCH 02/10] Added custom pull to refresh to look close to default iOS tableview version --- Common/UI/MITPullToRefresh/MITPullToRefresh.h | 18 + Common/UI/MITPullToRefresh/MITPullToRefresh.m | 330 ++++++++++++++++++ .../mit_ptrf_loading_icon@2x.png | Bin 0 -> 9941 bytes MIT Mobile.xcodeproj/project.pbxproj | 18 + 4 files changed, 366 insertions(+) create mode 100644 Common/UI/MITPullToRefresh/MITPullToRefresh.h create mode 100644 Common/UI/MITPullToRefresh/MITPullToRefresh.m create mode 100644 Common/UI/MITPullToRefresh/mit_ptrf_loading_icon@2x.png 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..087c97d3e --- /dev/null +++ b/Common/UI/MITPullToRefresh/MITPullToRefresh.m @@ -0,0 +1,330 @@ +#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 + +@interface MITPullToRefreshView () + +@property (nonatomic, assign) BOOL isActive; +@property (nonatomic, assign) MITPullToRefreshState state; + +@property (nonatomic, weak) UIScrollView *scrollView; +@property (nonatomic, assign) UIEdgeInsets unmodifiedInsets; + +@property (nonatomic, strong) UIImageView *progressView; +@property (nonatomic, strong) CAShapeLayer *maskLayer; +@property (nonatomic, strong) UIActivityIndicatorView *activityIndicatorView; + +@end + +#pragma mark MITPullToRefreshView + +@implementation MITPullToRefreshView : UIView + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + + if (self) { + self.autoresizingMask = UIViewAutoresizingFlexibleWidth; + self.state = MITPullToRefreshStateStopped; + + self.progressView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"mit_ptrf_loading_icon"]]; + self.progressView.frame = CGRectMake(0, 0, 25, 25); + self.progressView.alpha = 0; + [self addSubview:self.progressView]; + + self.maskLayer = [CAShapeLayer layer]; + self.maskLayer.frame = self.progressView.frame; + + self.activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + [self addSubview:self.activityIndicatorView]; + } + + return self; +} + +- (void)layoutSubviews +{ + CGRect progressViewBounds = [self.progressView bounds]; + CGPoint origin = CGPointMake(roundf((CGRectGetWidth(self.bounds) - CGRectGetWidth(progressViewBounds)) / 2), roundf(( 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.activityIndicatorView.frame = self.progressView.frame; +} + +- (void)activateWithScrollView:(UIScrollView *)scrollView +{ + if (self.isActive) { + return; + } + + self.isActive = YES; + self.scrollView = scrollView; + self.unmodifiedInsets = scrollView.contentInset; + [self observeScrollView:scrollView]; +} + +- (void)deactivate +{ + if (!self.isActive) { + return; + } + + self.isActive = NO; + [self unobserveScrollView:self.scrollView]; + self.scrollView = nil; +} + +- (void)startLoading +{ + self.state = MITPullToRefreshStateLoading; + [self setScrollViewContentInsetForLoadingAnimated:YES]; + self.progressView.alpha = 0; + self.activityIndicatorView.alpha = 1; + [self.activityIndicatorView startAnimating]; + self.pullToRefreshActionHandler(); +} + +- (void)stopAnimating +{ + self.state = MITPullToRefreshStateStopped; + [self resetScrollViewContentInsetAnimated:YES]; + self.activityIndicatorView.alpha = 0; + [self updateViewForProgress:0]; + [self.activityIndicatorView stopAnimating]; +} + +#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]; + } + break; + } + case MITPullToRefreshStateLoading: { + [self setScrollViewContentInsetForLoadingAnimated:NO]; + break; + } + } + + if (self.state == MITPullToRefreshStateLoading) { + [self setScrollViewContentInsetForLoadingAnimated:NO]; + } else { + CGFloat pullingDownHeight = -1 * (contentOffset.y + self.unmodifiedInsets.top); + + if (!self.scrollView.isDragging && self.state == MITPullToRefreshStateTriggered) { + [self startLoading]; + } else if (pullingDownHeight >= MITPullToRefreshTriggerHeight && self.scrollView.isDragging && self.state == MITPullToRefreshStateStopped) { + self.state = MITPullToRefreshStateTriggered; + } else if (pullingDownHeight < MITPullToRefreshTriggerHeight && self.state != MITPullToRefreshStateStopped) { + self.state = MITPullToRefreshStateStopped; + } + + 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; + + [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(CGRectGetWidth(self.maskLayer.frame) / 2.0, CGRectGetWidth(self.maskLayer.frame) / 2.0); + CGFloat radius = CGRectGetWidth(self.maskLayer.frame) / 2.0; + + UIBezierPath *circleMaskPath = [UIBezierPath bezierPath]; + [circleMaskPath addArcWithCenter:maskCenter radius:radius startAngle:startRadians endAngle:(startRadians + progressRadians) clockwise:YES]; + [circleMaskPath addLineToPoint:maskCenter]; + [circleMaskPath closePath]; + + self.maskLayer.path = circleMaskPath.CGPath; + + self.progressView.layer.mask = self.maskLayer; + + self.progressView.alpha = MIN(1, (progress / alphaThreshold)); + + [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_icon@2x.png b/Common/UI/MITPullToRefresh/mit_ptrf_loading_icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..925c3dec48ca0069dee70cf661fbff441911566c GIT binary patch literal 9941 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 Date: Mon, 6 Apr 2015 23:05:06 -0400 Subject: [PATCH 03/10] MITPullToRefresh now makes the scrollview always bounce vertically so that you can pull even when the content is smaller than the scrollview --- Common/UI/MITPullToRefresh/MITPullToRefresh.m | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Common/UI/MITPullToRefresh/MITPullToRefresh.m b/Common/UI/MITPullToRefresh/MITPullToRefresh.m index 087c97d3e..eb945bd80 100644 --- a/Common/UI/MITPullToRefresh/MITPullToRefresh.m +++ b/Common/UI/MITPullToRefresh/MITPullToRefresh.m @@ -102,6 +102,7 @@ @interface MITPullToRefreshView () @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; @@ -156,6 +157,10 @@ - (void)activateWithScrollView:(UIScrollView *)scrollView self.isActive = YES; self.scrollView = scrollView; self.unmodifiedInsets = scrollView.contentInset; + + self.originalAlwaysBounceVertical = scrollView.alwaysBounceVertical; + scrollView.alwaysBounceVertical = YES; + [self observeScrollView:scrollView]; } @@ -167,6 +172,7 @@ - (void)deactivate self.isActive = NO; [self unobserveScrollView:self.scrollView]; + self.scrollView.alwaysBounceVertical = self.originalAlwaysBounceVertical; self.scrollView = nil; } From 6576afa2b63daa46ac87445e2527b94c5c5acabb Mon Sep 17 00:00:00 2001 From: Ross LeBeau Date: Mon, 6 Apr 2015 23:05:44 -0400 Subject: [PATCH 04/10] Added pull to refresh to top of Shuttles route/map/stop container when in route state --- ...uttleRouteStopMapContainerViewController.m | 17 ++++++++ .../MITShuttleRouteViewController.h | 5 +++ .../MITShuttleRouteViewController.m | 42 +++++++++++++++++-- 3 files changed, 61 insertions(+), 3 deletions(-) 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..177888c77 100644 --- a/Modules/ShuttleTrack/MITShuttleRouteViewController.m +++ b/Modules/ShuttleTrack/MITShuttleRouteViewController.m @@ -39,6 +39,7 @@ - (instancetype)initWithRoute:(MITShuttleRoute *)route self = [super initWithStyle:UITableViewStylePlain]; if (self) { _route = route; + _shouldShowRefreshControl = YES; } return self; } @@ -101,9 +102,9 @@ - (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 @@ -121,13 +122,48 @@ - (void)predictionsDidUpdate } } +- (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]; }]; } From 431ee8879bb37b3a99cb7374166d3fc2e73da9e8 Mon Sep 17 00:00:00 2001 From: Ross LeBeau Date: Mon, 6 Apr 2015 23:06:48 -0400 Subject: [PATCH 05/10] Removed unused variable in shuttles route vc --- Modules/ShuttleTrack/MITShuttleRouteViewController.m | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Modules/ShuttleTrack/MITShuttleRouteViewController.m b/Modules/ShuttleTrack/MITShuttleRouteViewController.m index 177888c77..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 @@ -67,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]; } @@ -109,14 +105,8 @@ - (void)setupTableView #pragma mark - Update Data -- (void)predictionsWillUpdate -{ - self.isUpdating = YES; -} - - (void)predictionsDidUpdate { - self.isUpdating = NO; if (!self.shouldSuppressPredictionRefreshReloads) { [self.tableView reloadDataAndMaintainSelection]; } @@ -158,7 +148,6 @@ - (void)addRefreshControl - (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:)]) { From 147dbde67c1838e99c1ac48f707e5eddfc84d340 Mon Sep 17 00:00:00 2001 From: Ross LeBeau Date: Tue, 7 Apr 2015 11:34:46 -0400 Subject: [PATCH 06/10] Added animations to more closely mimic native pull to refresh --- Common/UI/MITPullToRefresh/MITPullToRefresh.m | 106 +++++++++++++++--- .../mit_ptrf_loading_wheel@2x.png | Bin 0 -> 10556 bytes ...@2x.png => mit_ptrf_progress_wheel@2x.png} | Bin MIT Mobile.xcodeproj/project.pbxproj | 12 +- 4 files changed, 96 insertions(+), 22 deletions(-) create mode 100644 Common/UI/MITPullToRefresh/mit_ptrf_loading_wheel@2x.png rename Common/UI/MITPullToRefresh/{mit_ptrf_loading_icon@2x.png => mit_ptrf_progress_wheel@2x.png} (100%) diff --git a/Common/UI/MITPullToRefresh/MITPullToRefresh.m b/Common/UI/MITPullToRefresh/MITPullToRefresh.m index eb945bd80..16622fd34 100644 --- a/Common/UI/MITPullToRefresh/MITPullToRefresh.m +++ b/Common/UI/MITPullToRefresh/MITPullToRefresh.m @@ -95,6 +95,13 @@ - (MITPullToRefreshState)mit_pullToRefreshState #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; @@ -106,7 +113,7 @@ @interface MITPullToRefreshView () @property (nonatomic, strong) UIImageView *progressView; @property (nonatomic, strong) CAShapeLayer *maskLayer; -@property (nonatomic, strong) UIActivityIndicatorView *activityIndicatorView; +@property (nonatomic, strong) UIImageView *loadingView; @end @@ -122,16 +129,18 @@ - (id)initWithFrame:(CGRect)frame self.autoresizingMask = UIViewAutoresizingFlexibleWidth; self.state = MITPullToRefreshStateStopped; - self.progressView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"mit_ptrf_loading_icon"]]; + self.progressView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"mit_ptrf_progress_wheel"]]; self.progressView.frame = CGRectMake(0, 0, 25, 25); self.progressView.alpha = 0; [self addSubview:self.progressView]; + self.loadingView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"mit_ptrf_loading_wheel"]]; + self.loadingView.frame = self.progressView.frame; + self.loadingView.alpha = 0; + [self addSubview:self.loadingView]; + self.maskLayer = [CAShapeLayer layer]; self.maskLayer.frame = self.progressView.frame; - - self.activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; - [self addSubview:self.activityIndicatorView]; } return self; @@ -145,7 +154,7 @@ - (void)layoutSubviews self.progressView.frame = progressViewFrame; self.maskLayer.frame = self.progressView.bounds; - self.activityIndicatorView.frame = self.progressView.frame; + self.loadingView.frame = self.progressView.frame; } - (void)activateWithScrollView:(UIScrollView *)scrollView @@ -180,19 +189,80 @@ - (void)startLoading { self.state = MITPullToRefreshStateLoading; [self setScrollViewContentInsetForLoadingAnimated:YES]; - self.progressView.alpha = 0; - self.activityIndicatorView.alpha = 1; - [self.activityIndicatorView startAnimating]; + [self startAnimating]; self.pullToRefreshActionHandler(); } +- (void)startAnimating +{ + // Rotate and fade out progress view + CABasicAnimation *progressSmoothRotationAnimation = [CABasicAnimation animation]; + progressSmoothRotationAnimation.keyPath = @"transform.rotation.z"; + progressSmoothRotationAnimation.duration = 1.2; + progressSmoothRotationAnimation.fromValue = @0; + progressSmoothRotationAnimation.toValue = @(M_PI); + + CABasicAnimation *progressFadeOutAnimation = [CABasicAnimation animation]; + progressFadeOutAnimation.keyPath = @"alpha"; + progressFadeOutAnimation.duration = 1.0; + progressFadeOutAnimation.fromValue = @1; + progressFadeOutAnimation.toValue = @0; + + CAAnimationGroup *startingProgressViewRotationGroup = [[CAAnimationGroup alloc] init]; + startingProgressViewRotationGroup.animations = @[progressSmoothRotationAnimation, progressFadeOutAnimation]; + startingProgressViewRotationGroup.duration = 1.2; + + // Rotate and fade in loading view + CABasicAnimation *loadingSmoothRotationAnimation = [CABasicAnimation animation]; + loadingSmoothRotationAnimation.keyPath = @"transform.rotation.z"; + loadingSmoothRotationAnimation.duration = 1.2; + loadingSmoothRotationAnimation.fromValue = @0; + loadingSmoothRotationAnimation.toValue = @(M_PI); + + CABasicAnimation *loadingFadeOutAnimation = [CABasicAnimation animation]; + loadingFadeOutAnimation.keyPath = @"alpha"; + loadingFadeOutAnimation.duration = 1.0; + loadingFadeOutAnimation.fromValue = @0; + loadingFadeOutAnimation.toValue = @1; + + CAAnimationGroup *startingLoadingViewRotationGroup = [[CAAnimationGroup alloc] init]; + startingLoadingViewRotationGroup.animations = @[loadingSmoothRotationAnimation, loadingFadeOutAnimation]; + startingLoadingViewRotationGroup.duration = 1.2; + + // Use this to begin the choppy rotation animation after the starting animations finish + startingLoadingViewRotationGroup.delegate = self; + + [self.progressView.layer addAnimation:startingProgressViewRotationGroup forKey:nil]; + [self.loadingView.layer addAnimation:startingLoadingViewRotationGroup forKey:nil]; + self.progressView.alpha = 0; + self.loadingView.alpha = 1; + self.loadingView.transform = CGAffineTransformMakeRotation(M_PI); +} + - (void)stopAnimating { self.state = MITPullToRefreshStateStopped; [self resetScrollViewContentInsetAnimated:YES]; - self.activityIndicatorView.alpha = 0; + self.loadingView.alpha = 0; + [self.loadingView.layer removeAnimationForKey:LoadingViewChoppyRotationKey]; [self updateViewForProgress:0]; - [self.activityIndicatorView stopAnimating]; +} + +#pragma mark CAAnimation Delegate + +- (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)finished +{ + // Begin choppy loading rotation animation + 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.loadingView.layer addAnimation:loadingChoppyRotationAnimation forKey:LoadingViewChoppyRotationKey]; } #pragma mark KVO @@ -266,6 +336,8 @@ - (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]; @@ -283,17 +355,15 @@ - (void)updateViewForProgress:(CGFloat)progress CGPoint maskCenter = CGPointMake(CGRectGetWidth(self.maskLayer.frame) / 2.0, CGRectGetWidth(self.maskLayer.frame) / 2.0); CGFloat radius = CGRectGetWidth(self.maskLayer.frame) / 2.0; - UIBezierPath *circleMaskPath = [UIBezierPath bezierPath]; - [circleMaskPath addArcWithCenter:maskCenter radius:radius startAngle:startRadians endAngle:(startRadians + progressRadians) clockwise:YES]; - [circleMaskPath addLineToPoint:maskCenter]; - [circleMaskPath closePath]; + UIBezierPath *progressMaskPath = [UIBezierPath bezierPath]; + [progressMaskPath addArcWithCenter:maskCenter radius:radius startAngle:startRadians endAngle:(startRadians + progressRadians) clockwise:YES]; + [progressMaskPath addLineToPoint:maskCenter]; + [progressMaskPath closePath]; - self.maskLayer.path = circleMaskPath.CGPath; + self.maskLayer.path = progressMaskPath.CGPath; self.progressView.layer.mask = self.maskLayer; - self.progressView.alpha = MIN(1, (progress / alphaThreshold)); - [CATransaction commit]; } 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 0000000000000000000000000000000000000000..a612868f402165cbb60a0ead8cb74607ca60b1f3 GIT binary patch literal 10556 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 Date: Tue, 7 Apr 2015 11:41:56 -0400 Subject: [PATCH 07/10] Removed duplicate code and refactored for readability --- Common/UI/MITPullToRefresh/MITPullToRefresh.m | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/Common/UI/MITPullToRefresh/MITPullToRefresh.m b/Common/UI/MITPullToRefresh/MITPullToRefresh.m index 16622fd34..49115936d 100644 --- a/Common/UI/MITPullToRefresh/MITPullToRefresh.m +++ b/Common/UI/MITPullToRefresh/MITPullToRefresh.m @@ -303,6 +303,8 @@ - (void)scrollViewDidScroll:(CGPoint)contentOffset case MITPullToRefreshStateTriggered: { if (!self.scrollView.isDragging) { [self startLoading]; + } else if (pullingDownHeight < MITPullToRefreshTriggerHeight) { + self.state = MITPullToRefreshStateStopped; } break; } @@ -312,22 +314,8 @@ - (void)scrollViewDidScroll:(CGPoint)contentOffset } } - if (self.state == MITPullToRefreshStateLoading) { - [self setScrollViewContentInsetForLoadingAnimated:NO]; - } else { - CGFloat pullingDownHeight = -1 * (contentOffset.y + self.unmodifiedInsets.top); - - if (!self.scrollView.isDragging && self.state == MITPullToRefreshStateTriggered) { - [self startLoading]; - } else if (pullingDownHeight >= MITPullToRefreshTriggerHeight && self.scrollView.isDragging && self.state == MITPullToRefreshStateStopped) { - self.state = MITPullToRefreshStateTriggered; - } else if (pullingDownHeight < MITPullToRefreshTriggerHeight && self.state != MITPullToRefreshStateStopped) { - self.state = MITPullToRefreshStateStopped; - } - - if (pullingDownHeight > 0 && self.state != MITPullToRefreshStateLoading) { - [self updateViewForProgress:(pullingDownHeight * 1 / MITPullToRefreshTriggerHeight)]; - } + if (pullingDownHeight > 0 && self.state != MITPullToRefreshStateLoading) { + [self updateViewForProgress:(pullingDownHeight * 1 / MITPullToRefreshTriggerHeight)]; } } From 9d860bbaa791a76e611edf74b9e2763ea7ba3a2c Mon Sep 17 00:00:00 2001 From: Ross LeBeau Date: Tue, 7 Apr 2015 14:44:20 -0400 Subject: [PATCH 08/10] Refactoring and improving animations for MITPullToRefresh --- Common/UI/MITPullToRefresh/MITPullToRefresh.m | 69 +++++++++---------- 1 file changed, 31 insertions(+), 38 deletions(-) diff --git a/Common/UI/MITPullToRefresh/MITPullToRefresh.m b/Common/UI/MITPullToRefresh/MITPullToRefresh.m index 49115936d..7f6a19f66 100644 --- a/Common/UI/MITPullToRefresh/MITPullToRefresh.m +++ b/Common/UI/MITPullToRefresh/MITPullToRefresh.m @@ -129,16 +129,16 @@ - (id)initWithFrame:(CGRect)frame self.autoresizingMask = UIViewAutoresizingFlexibleWidth; self.state = MITPullToRefreshStateStopped; - self.progressView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"mit_ptrf_progress_wheel"]]; - self.progressView.frame = CGRectMake(0, 0, 25, 25); - self.progressView.alpha = 0; - [self addSubview:self.progressView]; - self.loadingView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"mit_ptrf_loading_wheel"]]; self.loadingView.frame = self.progressView.frame; self.loadingView.alpha = 0; [self addSubview:self.loadingView]; + self.progressView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"mit_ptrf_progress_wheel"]]; + self.progressView.frame = CGRectMake(0, 0, 25, 25); + self.progressView.alpha = 0; + [self addSubview:self.progressView]; + self.maskLayer = [CAShapeLayer layer]; self.maskLayer.frame = self.progressView.frame; } @@ -195,45 +195,37 @@ - (void)startLoading - (void)startAnimating { + CAMediaTimingFunction *rotationTiming = [CAMediaTimingFunction functionWithControlPoints:0.2 :0.1 :0.8 :0.9]; + // Rotate and fade out progress view - CABasicAnimation *progressSmoothRotationAnimation = [CABasicAnimation animation]; - progressSmoothRotationAnimation.keyPath = @"transform.rotation.z"; - progressSmoothRotationAnimation.duration = 1.2; - progressSmoothRotationAnimation.fromValue = @0; - progressSmoothRotationAnimation.toValue = @(M_PI); + CABasicAnimation *progressRotation = [CABasicAnimation animation]; + progressRotation.keyPath = @"transform.rotation.z"; + progressRotation.duration = 1.2; + progressRotation.fromValue = @0; + progressRotation.toValue = @(M_PI); + progressRotation.timingFunction = rotationTiming; - CABasicAnimation *progressFadeOutAnimation = [CABasicAnimation animation]; - progressFadeOutAnimation.keyPath = @"alpha"; - progressFadeOutAnimation.duration = 1.0; - progressFadeOutAnimation.fromValue = @1; - progressFadeOutAnimation.toValue = @0; + CABasicAnimation *progressFadeOut = [CABasicAnimation animation]; + progressFadeOut.keyPath = @"opacity"; + progressFadeOut.duration = 1.0; + progressFadeOut.fromValue = @1; + progressFadeOut.toValue = @0; - CAAnimationGroup *startingProgressViewRotationGroup = [[CAAnimationGroup alloc] init]; - startingProgressViewRotationGroup.animations = @[progressSmoothRotationAnimation, progressFadeOutAnimation]; - startingProgressViewRotationGroup.duration = 1.2; + CAAnimationGroup *progressViewRotationGroup = [[CAAnimationGroup alloc] init]; + progressViewRotationGroup.animations = @[progressRotation, progressFadeOut]; + progressViewRotationGroup.duration = 1.2; // Rotate and fade in loading view - CABasicAnimation *loadingSmoothRotationAnimation = [CABasicAnimation animation]; - loadingSmoothRotationAnimation.keyPath = @"transform.rotation.z"; - loadingSmoothRotationAnimation.duration = 1.2; - loadingSmoothRotationAnimation.fromValue = @0; - loadingSmoothRotationAnimation.toValue = @(M_PI); - - CABasicAnimation *loadingFadeOutAnimation = [CABasicAnimation animation]; - loadingFadeOutAnimation.keyPath = @"alpha"; - loadingFadeOutAnimation.duration = 1.0; - loadingFadeOutAnimation.fromValue = @0; - loadingFadeOutAnimation.toValue = @1; - - CAAnimationGroup *startingLoadingViewRotationGroup = [[CAAnimationGroup alloc] init]; - startingLoadingViewRotationGroup.animations = @[loadingSmoothRotationAnimation, loadingFadeOutAnimation]; - startingLoadingViewRotationGroup.duration = 1.2; - - // Use this to begin the choppy rotation animation after the starting animations finish - startingLoadingViewRotationGroup.delegate = self; + CABasicAnimation *loadingRotation = [CABasicAnimation animation]; + loadingRotation.keyPath = @"transform.rotation.z"; + loadingRotation.duration = 1.2; + loadingRotation.fromValue = @0; + loadingRotation.toValue = @(M_PI); + loadingRotation.timingFunction = rotationTiming; + loadingRotation.delegate = self; - [self.progressView.layer addAnimation:startingProgressViewRotationGroup forKey:nil]; - [self.loadingView.layer addAnimation:startingLoadingViewRotationGroup forKey:nil]; + [self.progressView.layer addAnimation:progressViewRotationGroup forKey:nil]; + [self.loadingView.layer addAnimation:loadingRotation forKey:nil]; self.progressView.alpha = 0; self.loadingView.alpha = 1; self.loadingView.transform = CGAffineTransformMakeRotation(M_PI); @@ -245,6 +237,7 @@ - (void)stopAnimating [self resetScrollViewContentInsetAnimated:YES]; self.loadingView.alpha = 0; [self.loadingView.layer removeAnimationForKey:LoadingViewChoppyRotationKey]; + self.loadingView.transform = CGAffineTransformIdentity; [self updateViewForProgress:0]; } From 5f69d91830a7ed1dffd369fe903336dc96ce3d65 Mon Sep 17 00:00:00 2001 From: Ross LeBeau Date: Wed, 8 Apr 2015 15:56:46 -0400 Subject: [PATCH 09/10] Added ending animation to pull-to-refresh --- Common/UI/MITPullToRefresh/MITPullToRefresh.m | 103 +++++++++++++----- 1 file changed, 74 insertions(+), 29 deletions(-) diff --git a/Common/UI/MITPullToRefresh/MITPullToRefresh.m b/Common/UI/MITPullToRefresh/MITPullToRefresh.m index 7f6a19f66..e30176317 100644 --- a/Common/UI/MITPullToRefresh/MITPullToRefresh.m +++ b/Common/UI/MITPullToRefresh/MITPullToRefresh.m @@ -129,13 +129,15 @@ - (id)initWithFrame:(CGRect)frame 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 = self.progressView.frame; + 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 = CGRectMake(0, 0, 25, 25); + self.progressView.frame = wheelFrame; self.progressView.alpha = 0; [self addSubview:self.progressView]; @@ -149,7 +151,7 @@ - (id)initWithFrame:(CGRect)frame - (void)layoutSubviews { CGRect progressViewBounds = [self.progressView bounds]; - CGPoint origin = CGPointMake(roundf((CGRectGetWidth(self.bounds) - CGRectGetWidth(progressViewBounds)) / 2), roundf(( CGRectGetHeight(self.bounds) - CGRectGetHeight(progressViewBounds)) / 2)); + 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; @@ -195,49 +197,81 @@ - (void)startLoading - (void)startAnimating { - CAMediaTimingFunction *rotationTiming = [CAMediaTimingFunction functionWithControlPoints:0.2 :0.1 :0.8 :0.9]; + CAMediaTimingFunction *rotationTiming = [CAMediaTimingFunction functionWithControlPoints:0.3 :0.1 :0.7 :0.9]; // Rotate and fade out progress view CABasicAnimation *progressRotation = [CABasicAnimation animation]; progressRotation.keyPath = @"transform.rotation.z"; - progressRotation.duration = 1.2; + progressRotation.duration = 0.7; progressRotation.fromValue = @0; - progressRotation.toValue = @(M_PI); + progressRotation.toValue = @(M_PI_2); progressRotation.timingFunction = rotationTiming; CABasicAnimation *progressFadeOut = [CABasicAnimation animation]; progressFadeOut.keyPath = @"opacity"; - progressFadeOut.duration = 1.0; + progressFadeOut.duration = 0.65; progressFadeOut.fromValue = @1; progressFadeOut.toValue = @0; CAAnimationGroup *progressViewRotationGroup = [[CAAnimationGroup alloc] init]; progressViewRotationGroup.animations = @[progressRotation, progressFadeOut]; - progressViewRotationGroup.duration = 1.2; + progressViewRotationGroup.duration = 0.7; - // Rotate and fade in loading view + // 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.2; - loadingRotation.fromValue = @0; - loadingRotation.toValue = @(M_PI); + loadingRotation.duration = 0.7; + loadingRotation.fromValue = @(M_PI); + loadingRotation.toValue = @(3.0 * M_PI_2); loadingRotation.timingFunction = rotationTiming; loadingRotation.delegate = self; + loadingRotation.removedOnCompletion = NO; [self.progressView.layer addAnimation:progressViewRotationGroup forKey:nil]; - [self.loadingView.layer addAnimation:loadingRotation forKey:nil]; + [self.loadingView.layer addAnimation:loadingRotation forKey:StartingLoadingViewAnimationGroupKey]; + self.progressView.alpha = 0; self.loadingView.alpha = 1; - self.loadingView.transform = CGAffineTransformMakeRotation(M_PI); + self.loadingView.transform = CGAffineTransformMakeRotation(3.0 * M_PI_2); } - (void)stopAnimating { self.state = MITPullToRefreshStateStopped; [self resetScrollViewContentInsetAnimated:YES]; - self.loadingView.alpha = 0; + [self.loadingView.layer removeAnimationForKey:LoadingViewChoppyRotationKey]; - self.loadingView.transform = CGAffineTransformIdentity; + + 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.8; + + CAAnimationGroup *endingAnimationGroup = [[CAAnimationGroup alloc] init]; + endingAnimationGroup.animations = @[loadingRotation, loadingSize]; + endingAnimationGroup.duration = endingAnimationDuration; + endingAnimationGroup.delegate = self; + endingAnimationGroup.removedOnCompletion = NO; + + [self.loadingView.layer addAnimation:endingAnimationGroup forKey:EndingLoadingViewAnimationGroupKey]; + [self updateViewForProgress:0]; } @@ -245,17 +279,28 @@ - (void)stopAnimating - (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)finished { - // Begin choppy loading rotation animation - 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.loadingView.layer addAnimation:loadingChoppyRotationAnimation forKey:LoadingViewChoppyRotationKey]; + if ([animation isEqual:[self.loadingView.layer animationForKey:StartingLoadingViewAnimationGroupKey]]) { + [self.loadingView.layer removeAnimationForKey:StartingLoadingViewAnimationGroupKey]; + + // Begin choppy loading rotation animation + 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.loadingView.layer addAnimation:loadingChoppyRotationAnimation forKey:LoadingViewChoppyRotationKey]; + } else if ([animation isEqual:[self.loadingView.layer animationForKey:EndingLoadingViewAnimationGroupKey]]) { + [self.loadingView.layer removeAnimationForKey:EndingLoadingViewAnimationGroupKey]; + + self.loadingView.alpha = 0; + self.loadingView.transform = CGAffineTransformIdentity; + } else { + NSLog(@"none"); + } } #pragma mark KVO @@ -333,8 +378,8 @@ - (void)updateViewForProgress:(CGFloat)progress // 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(CGRectGetWidth(self.maskLayer.frame) / 2.0, CGRectGetWidth(self.maskLayer.frame) / 2.0); - CGFloat radius = CGRectGetWidth(self.maskLayer.frame) / 2.0; + 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]; From 67951432aa05b4962fd7a9b26cc4a76e32861765 Mon Sep 17 00:00:00 2001 From: Ross LeBeau Date: Thu, 9 Apr 2015 12:42:32 -0400 Subject: [PATCH 10/10] Improved animations and robustness of pull to refresh --- Common/UI/MITPullToRefresh/MITPullToRefresh.m | 74 +++++++------------ 1 file changed, 28 insertions(+), 46 deletions(-) diff --git a/Common/UI/MITPullToRefresh/MITPullToRefresh.m b/Common/UI/MITPullToRefresh/MITPullToRefresh.m index e30176317..e1098ca2f 100644 --- a/Common/UI/MITPullToRefresh/MITPullToRefresh.m +++ b/Common/UI/MITPullToRefresh/MITPullToRefresh.m @@ -197,19 +197,19 @@ - (void)startLoading - (void)startAnimating { - CAMediaTimingFunction *rotationTiming = [CAMediaTimingFunction functionWithControlPoints:0.3 :0.1 :0.7 :0.9]; + 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 = 0.7; + progressRotation.duration = 1.4; progressRotation.fromValue = @0; progressRotation.toValue = @(M_PI_2); progressRotation.timingFunction = rotationTiming; CABasicAnimation *progressFadeOut = [CABasicAnimation animation]; progressFadeOut.keyPath = @"opacity"; - progressFadeOut.duration = 0.65; + progressFadeOut.duration = 1; progressFadeOut.fromValue = @1; progressFadeOut.toValue = @0; @@ -220,19 +220,28 @@ - (void)startAnimating // 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 = 0.7; - loadingRotation.fromValue = @(M_PI); - loadingRotation.toValue = @(3.0 * M_PI_2); + loadingRotation.duration = 1.4; + loadingRotation.fromValue = @(0); + loadingRotation.toValue = @(M_PI_2); loadingRotation.timingFunction = rotationTiming; - loadingRotation.delegate = self; - loadingRotation.removedOnCompletion = NO; - [self.progressView.layer addAnimation:progressViewRotationGroup forKey:nil]; - [self.loadingView.layer addAnimation:loadingRotation forKey:StartingLoadingViewAnimationGroupKey]; + 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(3.0 * M_PI_2); + self.loadingView.transform = CGAffineTransformMakeRotation(M_PI_2); } - (void)stopAnimating @@ -240,6 +249,10 @@ - (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; @@ -251,7 +264,6 @@ - (void)stopAnimating loadingRotation.toValue = @(M_PI_2); loadingRotation.additive = YES; - CABasicAnimation *loadingSize = [CABasicAnimation animation]; loadingSize.keyPath = @"transform.scale"; loadingSize.duration = endingAnimationDuration; @@ -262,45 +274,15 @@ - (void)stopAnimating loadingFadeOut.keyPath = @"opacity"; loadingFadeOut.duration = endingAnimationDuration; loadingFadeOut.fromValue = @1; - loadingFadeOut.toValue = @0.8; + loadingFadeOut.toValue = @0.1; CAAnimationGroup *endingAnimationGroup = [[CAAnimationGroup alloc] init]; - endingAnimationGroup.animations = @[loadingRotation, loadingSize]; + endingAnimationGroup.animations = @[loadingRotation, loadingSize, loadingFadeOut]; endingAnimationGroup.duration = endingAnimationDuration; - endingAnimationGroup.delegate = self; - endingAnimationGroup.removedOnCompletion = NO; - [self.loadingView.layer addAnimation:endingAnimationGroup forKey:EndingLoadingViewAnimationGroupKey]; + [self.loadingView.layer addAnimation:endingAnimationGroup forKey:nil]; - [self updateViewForProgress:0]; -} - -#pragma mark CAAnimation Delegate - -- (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)finished -{ - if ([animation isEqual:[self.loadingView.layer animationForKey:StartingLoadingViewAnimationGroupKey]]) { - [self.loadingView.layer removeAnimationForKey:StartingLoadingViewAnimationGroupKey]; - - // Begin choppy loading rotation animation - 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.loadingView.layer addAnimation:loadingChoppyRotationAnimation forKey:LoadingViewChoppyRotationKey]; - } else if ([animation isEqual:[self.loadingView.layer animationForKey:EndingLoadingViewAnimationGroupKey]]) { - [self.loadingView.layer removeAnimationForKey:EndingLoadingViewAnimationGroupKey]; - - self.loadingView.alpha = 0; - self.loadingView.transform = CGAffineTransformIdentity; - } else { - NSLog(@"none"); - } + self.loadingView.alpha = 0; } #pragma mark KVO