// // UINavigationController+M13ProgressViewBar.m // M13ProgressView // #import "UINavigationController+M13ProgressViewBar.h" #import "UIApplication+M13ProgressSuite.h" #import <objc/runtime.h> //Keys to set properties since one cannot define properties in a category. static char oldTitleKey; static char displayLinkKey; static char animationFromKey; static char animationToKey; static char animationStartTimeKey; static char progressKey; static char progressViewKey; static char indeterminateKey; static char indeterminateLayerKey; static char isShowingProgressKey; static char primaryColorKey; static char secondaryColorKey; static char backgroundColorKey; static char backgroundViewKey; @implementation UINavigationController (M13ProgressViewBar) #pragma mark Title - (void)setProgressTitle:(NSString *)title { //Change the title on screen. NSString *oldTitle = [self getOldTitle]; if (oldTitle == nil) { //We haven't changed the navigation bar yet. So store the original before changing it. [self setOldTitle:self.visibleViewController.navigationItem.title]; } if (title != nil) { self.visibleViewController.navigationItem.title = title; } else { self.visibleViewController.navigationItem.title = oldTitle; [self setOldTitle:nil]; } } #pragma mark Progress - (void)setProgress:(CGFloat)progress animated:(BOOL)animated { CADisplayLink *displayLink = [self getDisplayLink]; if (animated == NO) { if (displayLink) { //Kill running animations [displayLink invalidate]; [self setDisplayLink:nil]; } [self setProgress:progress]; } else { [self setAnimationStartTime:CACurrentMediaTime()]; [self setAnimationFromValue:[self getProgress]]; [self setAnimationToValue:progress]; if (!displayLink) { //Create and setup the display link [displayLink invalidate]; displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(animateProgress:)]; [self setDisplayLink:displayLink]; [displayLink addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes]; } /*else { //Reuse the current display link }*/ } } - (void)animateProgress:(CADisplayLink *)displayLink { dispatch_async(dispatch_get_main_queue(), ^{ CGFloat dt = (displayLink.timestamp - [self getAnimationStartTime]) / [self getAnimationDuration]; if (dt >= 1.0) { //Order is important! Otherwise concurrency will cause errors, because setProgress: will detect an animation in progress and try to stop it by itself. Once over one, set to actual progress amount. Animation is over. [displayLink invalidate]; [self setDisplayLink:nil]; [self setProgress:[self getAnimationToValue]]; return; } //Set progress [self setProgress:[self getAnimationFromValue] + dt * ([self getAnimationToValue] - [self getAnimationFromValue])]; }); } - (void)finishProgress { UIView *progressView = [self getProgressView]; UIView *backgroundView = [self getBackgroundView]; if (progressView && backgroundView) { dispatch_async(dispatch_get_main_queue(), ^{ [UIView animateWithDuration:0.1 animations:^{ CGRect progressFrame = progressView.frame; progressFrame.size.width = self.navigationBar.frame.size.width; progressView.frame = progressFrame; } completion:^(BOOL finished) { [UIView animateWithDuration:0.5 animations:^{ progressView.alpha = 0; backgroundView.alpha = 0; } completion:^(BOOL finished) { [progressView removeFromSuperview]; [backgroundView removeFromSuperview]; backgroundView.alpha = 1; progressView.alpha = 1; [self setTitle:nil]; [self setIsShowingProgressBar:NO]; }]; }]; }); } } - (void)cancelProgress { UIView *progressView = [self getProgressView]; UIView *backgroundView = [self getBackgroundView]; if (progressView && backgroundView) { dispatch_async(dispatch_get_main_queue(), ^{ [UIView animateWithDuration:0.5 animations:^{ progressView.alpha = 0; backgroundView.alpha = 0; } completion:^(BOOL finished) { [progressView removeFromSuperview]; [backgroundView removeFromSuperview]; progressView.alpha = 1; backgroundView.alpha = 1; [self setTitle:nil]; [self setIsShowingProgressBar:NO]; }]; }); } } #pragma mark Orientation - (UIInterfaceOrientation)currentDeviceOrientation { UIInterfaceOrientation orientation; if ([UIApplication isM13AppExtension]) { if ([UIScreen mainScreen].bounds.size.width < [UIScreen mainScreen].bounds.size.height) { orientation = UIInterfaceOrientationPortrait; } else { orientation = UIInterfaceOrientationLandscapeLeft; } } else { orientation = [UIApplication safeM13SharedApplication].statusBarOrientation; } return orientation; } #pragma mark Drawing - (void)showProgress { UIView *progressView = [self getProgressView]; UIView *backgroundView = [self getBackgroundView]; [UIView animateWithDuration:.1 animations:^{ progressView.alpha = 1; backgroundView.alpha = 1; }]; [self setIsShowingProgressBar:YES]; } - (void)updateProgress { [self updateProgressWithInterfaceOrientation:[self currentDeviceOrientation]]; } - (void)updateProgressWithInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { //Create the progress view if it doesn't exist UIView *progressView = [self getProgressView]; if(!progressView) { progressView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 2.5)]; progressView.clipsToBounds = YES; [self setProgressView:progressView]; } if ([self getPrimaryColor]) { progressView.backgroundColor = [self getPrimaryColor]; } else { progressView.backgroundColor = self.navigationBar.tintColor; } //Create background view if it doesn't exist UIView *backgroundView = [self getBackgroundView]; if (!backgroundView) { backgroundView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 2.5)]; backgroundView.clipsToBounds = YES; [self setBackgroundView:backgroundView]; } if ([self getBackgroundColor]) { backgroundView.backgroundColor = [self getBackgroundColor]; } else { backgroundView.backgroundColor = [UIColor clearColor]; } //Calculate the frame of the navigation bar, based off the orientation. UIView *topView = self.topViewController.view; CGSize screenSize; if (topView) { screenSize = topView.bounds.size; } else { screenSize = [UIScreen mainScreen].bounds.size; } CGFloat width = 0.0; CGFloat height = 0.0; //Calculate the width of the screen if (UIInterfaceOrientationIsLandscape(interfaceOrientation)) { //Use the maximum value width = MAX(screenSize.width, screenSize.height); if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) { height = 32.0; //Hate hardcoding values, but autolayout doesn't work, and cant retreive the new height until after the animation completes. } else { height = 44.0; //Hate hardcoding values, but autolayout doesn't work, and cant retreive the new height until after the animation completes. } } else { //Use the minimum value width = MIN(screenSize.width, screenSize.height); height = 44.0; //Hate hardcoding values, but autolayout doesn't work, and cant retreive the new height until after the animation completes. } //Check if the progress view is in its superview and if we are showing the bar. if (progressView.superview == nil && [self isShowingProgressBar]) { [self.navigationBar addSubview:backgroundView]; [self.navigationBar addSubview:progressView]; } //Layout if (![self getIndeterminate]) { //Calculate the width of the progress view; float progressWidth = (float)width * (float)[self getProgress]; //Set the frame of the progress view progressView.frame = CGRectMake(0, height - 2.5, progressWidth, 2.5); } else { //Calculate the width of the progress view progressView.frame = CGRectMake(0, height - 2.5, width, 2.5); } backgroundView.frame = CGRectMake(0, height - 2.5, width, 2.5); } - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { [self updateProgressWithInterfaceOrientation:toInterfaceOrientation]; [self drawIndeterminateWithInterfaceOrientation:toInterfaceOrientation]; } - (void)drawIndeterminate { [self drawIndeterminateWithInterfaceOrientation:[self currentDeviceOrientation]]; } - (void)drawIndeterminateWithInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { if ([self getIndeterminate]) { //Get the indeterminate layer CALayer *indeterminateLayer = [self getIndeterminateLayer]; if (!indeterminateLayer) { //Create if needed indeterminateLayer = [CALayer layer]; [self setIndeterminateLayer:indeterminateLayer]; } //Calculate the frame of the navigation bar, based off the orientation. CGSize screenSize = [UIScreen mainScreen].bounds.size; CGFloat width = 0.0; //Calculate the width of the screen if (UIInterfaceOrientationIsLandscape(interfaceOrientation)) { //Use the maximum value width = MAX(screenSize.width, screenSize.height); } else { //Use the minimum value width = MIN(screenSize.width, screenSize.height); } //Create the pattern image CGFloat stripeWidth = 2.5; //Start the image context UIGraphicsBeginImageContextWithOptions(CGSizeMake(stripeWidth * 4.0, stripeWidth * 4.0), NO, [UIScreen mainScreen].scale); //Fill the background if ([self getPrimaryColor]) { [[self getPrimaryColor] setFill]; } else { [self.navigationBar.tintColor setFill]; } UIBezierPath *fillPath = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, stripeWidth * 4.0, stripeWidth * 4.0)]; [fillPath fill]; //Draw the stripes //Set the stripe color if ([self getSecondaryColor]) { [[self getSecondaryColor] setFill]; } else { CGFloat red; CGFloat green; CGFloat blue; CGFloat alpha; [self.navigationBar.barTintColor getRed:&red green:&green blue:&blue alpha:&alpha]; //System set the tint color to a close to, but not non-zero value for each component. if (alpha > .05) { [self.navigationBar.barTintColor setFill]; } else { [[UIColor whiteColor] setFill]; } } for (int i = 0; i < 4; i++) { //Create the four inital points of the fill shape CGPoint bottomLeft = CGPointMake(-(stripeWidth * 4.0), stripeWidth * 4.0); CGPoint topLeft = CGPointMake(0, 0); CGPoint topRight = CGPointMake(stripeWidth, 0); CGPoint bottomRight = CGPointMake(-(stripeWidth * 4.0) + stripeWidth, stripeWidth * 4.0); //Shift all four points as needed to draw all four stripes bottomLeft.x += i * (2 * stripeWidth); topLeft.x += i * (2 * stripeWidth); topRight.x += i * (2 * stripeWidth); bottomRight.x += i * (2 * stripeWidth); //Create the fill path UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:bottomLeft]; [path addLineToPoint:topLeft]; [path addLineToPoint:topRight]; [path addLineToPoint:bottomRight]; [path closePath]; [path fill]; } //Retreive the image UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); //Set the background of the progress layer indeterminateLayer.backgroundColor = [UIColor colorWithPatternImage:image].CGColor; //remove any indeterminate layer animations [indeterminateLayer removeAllAnimations]; //Set the indeterminate layer frame and add to the sub view indeterminateLayer.frame = CGRectMake(0, 0, width + (4 * 2.5), 2.5); UIView *progressView = [self getProgressView]; [progressView.layer addSublayer:indeterminateLayer]; //Add the animation CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"]; animation.duration = .1; animation.repeatCount = HUGE_VALF; animation.removedOnCompletion = YES; animation.fromValue = [NSValue valueWithCGPoint:CGPointMake(- (2 * 2.5) + (width / 2.0), 2.5 / 2.0)]; animation.toValue = [NSValue valueWithCGPoint:CGPointMake(0 + (width / 2.0), 2.5 / 2.0)]; [indeterminateLayer addAnimation:animation forKey:@"position"]; } else { CALayer *indeterminateLayer = [self getIndeterminateLayer]; [indeterminateLayer removeAllAnimations]; [indeterminateLayer removeFromSuperlayer]; } } #pragma mark properties - (void)setOldTitle:(NSString *)oldTitle { objc_setAssociatedObject(self, &oldTitleKey, self.visibleViewController.navigationItem.title, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSString *)getOldTitle { return objc_getAssociatedObject(self, &oldTitleKey); } - (void)setDisplayLink:(CADisplayLink *)displayLink { objc_setAssociatedObject(self, &displayLinkKey, displayLink, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (CADisplayLink *)getDisplayLink { return objc_getAssociatedObject(self, &displayLinkKey); } - (void)setAnimationFromValue:(CGFloat)animationFromValue { objc_setAssociatedObject(self, &animationFromKey, [NSNumber numberWithFloat:(float)animationFromValue], OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (CGFloat)getAnimationFromValue { NSNumber *number = objc_getAssociatedObject(self, &animationFromKey); return number.floatValue; } - (void)setAnimationToValue:(CGFloat)animationToValue { objc_setAssociatedObject(self, &animationToKey, [NSNumber numberWithFloat:(float)animationToValue], OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (CGFloat)getAnimationToValue { NSNumber *number = objc_getAssociatedObject(self, &animationToKey); return number.floatValue; } - (void)setAnimationStartTime:(NSTimeInterval)animationStartTime { objc_setAssociatedObject(self, &animationStartTimeKey, [NSNumber numberWithFloat:(float)animationStartTime], OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSTimeInterval)getAnimationStartTime { NSNumber *number = objc_getAssociatedObject(self, &animationStartTimeKey); return number.floatValue; } - (void)setProgress:(CGFloat)progress { if (progress > 1.0) { progress = 1.0; } else if (progress < 0.0) { progress = 0.0; } objc_setAssociatedObject(self, &progressKey, [NSNumber numberWithFloat:(float)progress], OBJC_ASSOCIATION_RETAIN_NONATOMIC); //Draw the update if ([NSThread isMainThread]) { [self updateProgress]; } else { //Sometimes UINavigationController runs in a background thread. And drawing is not thread safe. dispatch_async(dispatch_get_main_queue(), ^{ [self updateProgress]; }); } } - (void)setIsShowingProgressBar:(BOOL)isShowingProgressBar { objc_setAssociatedObject(self, &isShowingProgressKey, [NSNumber numberWithBool:isShowingProgressBar], OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (CGFloat)getProgress { NSNumber *number = objc_getAssociatedObject(self, &progressKey); return number.floatValue; } - (CGFloat)getAnimationDuration { return .3; } - (void)setProgressView:(UIView *)view { objc_setAssociatedObject(self, &progressViewKey, view, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (UIView *)getProgressView { return objc_getAssociatedObject(self, &progressViewKey); } - (void)setBackgroundView:(UIView *)view { objc_setAssociatedObject(self, &backgroundViewKey, view, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (UIView *)getBackgroundView { return objc_getAssociatedObject(self, &backgroundViewKey); } - (void)setIndeterminate:(BOOL)indeterminate { objc_setAssociatedObject(self, &indeterminateKey, [NSNumber numberWithBool:indeterminate], OBJC_ASSOCIATION_RETAIN_NONATOMIC); [self updateProgress]; [self drawIndeterminate]; } - (BOOL)getIndeterminate { NSNumber *number = objc_getAssociatedObject(self, &indeterminateKey); return number.boolValue; } - (void)setIndeterminateLayer:(CALayer *)indeterminateLayer { objc_setAssociatedObject(self, &indeterminateLayerKey, indeterminateLayer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (CALayer *)getIndeterminateLayer { return objc_getAssociatedObject(self, &indeterminateLayerKey); } - (BOOL)isShowingProgressBar { return [objc_getAssociatedObject(self, &isShowingProgressKey) boolValue]; } - (void)setPrimaryColor:(UIColor *)primaryColor { objc_setAssociatedObject(self, &primaryColorKey, primaryColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); [self getProgressView].backgroundColor = primaryColor; [self setIndeterminate:[self getIndeterminate]]; } - (UIColor *)getPrimaryColor { return objc_getAssociatedObject(self, &primaryColorKey); } - (void)setSecondaryColor:(UIColor *)secondaryColor { objc_setAssociatedObject(self, &secondaryColorKey, secondaryColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); [self setIndeterminate:[self getIndeterminate]]; } - (UIColor *)getSecondaryColor { return objc_getAssociatedObject(self, &secondaryColorKey); } - (void)setBackgroundColor:(UIColor *)backgroundColor { objc_setAssociatedObject(self, &backgroundColorKey, backgroundColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); [self setIndeterminate:[self getIndeterminate]]; } - (UIColor *)getBackgroundColor { return objc_getAssociatedObject(self, &backgroundColorKey); } @end