You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

542 lines
18 KiB

//
// 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