// // MBProgressHUD.m // Version 0.9.2 // Created by Matej Bukovinski on 2.4.09. // #import "MBProgressHUD.h" #import <tgmath.h> #if __has_feature(objc_arc) #define MB_AUTORELEASE(exp) exp #define MB_RELEASE(exp) exp #define MB_RETAIN(exp) exp #else #define MB_AUTORELEASE(exp) [exp autorelease] #define MB_RELEASE(exp) [exp release] #define MB_RETAIN(exp) [exp retain] #endif #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 60000 #define MBLabelAlignmentCenter NSTextAlignmentCenter #else #define MBLabelAlignmentCenter UITextAlignmentCenter #endif #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 #define MB_TEXTSIZE(text, font) [text length] > 0 ? [text \ sizeWithAttributes:@{NSFontAttributeName:font}] : CGSizeZero; #else #define MB_TEXTSIZE(text, font) [text length] > 0 ? [text sizeWithFont:font] : CGSizeZero; #endif #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 #define MB_MULTILINE_TEXTSIZE(text, font, maxSize, mode) [text length] > 0 ? [text \ boundingRectWithSize:maxSize options:(NSStringDrawingUsesLineFragmentOrigin) \ attributes:@{NSFontAttributeName:font} context:nil].size : CGSizeZero; #else #define MB_MULTILINE_TEXTSIZE(text, font, maxSize, mode) [text length] > 0 ? [text \ sizeWithFont:font constrainedToSize:maxSize lineBreakMode:mode] : CGSizeZero; #endif #ifndef kCFCoreFoundationVersionNumber_iOS_7_0 #define kCFCoreFoundationVersionNumber_iOS_7_0 847.20 #endif #ifndef kCFCoreFoundationVersionNumber_iOS_8_0 #define kCFCoreFoundationVersionNumber_iOS_8_0 1129.15 #endif static const CGFloat kPadding = 4.f; static const CGFloat kLabelFontSize = 16.f; static const CGFloat kDetailsLabelFontSize = 12.f; @interface MBProgressHUD () { BOOL useAnimation; SEL methodForExecution; id targetForExecution; id objectForExecution; UILabel *label; UILabel *detailsLabel; BOOL isFinished; CGAffineTransform rotationTransform; } @property (atomic, MB_STRONG) UIView *indicator; @property (atomic, MB_STRONG) NSTimer *graceTimer; @property (atomic, MB_STRONG) NSTimer *minShowTimer; @property (atomic, MB_STRONG) NSDate *showStarted; @end @implementation MBProgressHUD #pragma mark - Properties @synthesize animationType; @synthesize delegate; @synthesize opacity; @synthesize color; @synthesize labelFont; @synthesize labelColor; @synthesize detailsLabelFont; @synthesize detailsLabelColor; @synthesize indicator; @synthesize xOffset; @synthesize yOffset; @synthesize minSize; @synthesize square; @synthesize margin; @synthesize dimBackground; @synthesize graceTime; @synthesize minShowTime; @synthesize graceTimer; @synthesize minShowTimer; @synthesize taskInProgress; @synthesize removeFromSuperViewOnHide; @synthesize customView; @synthesize showStarted; @synthesize mode; @synthesize labelText; @synthesize detailsLabelText; @synthesize progress; @synthesize size; @synthesize activityIndicatorColor; #if NS_BLOCKS_AVAILABLE @synthesize completionBlock; #endif #pragma mark - Class methods + (MB_INSTANCETYPE)showHUDAddedTo:(UIView *)view animated:(BOOL)animated { MBProgressHUD *hud = [[self alloc] initWithView:view]; hud.removeFromSuperViewOnHide = YES; [view addSubview:hud]; [hud show:animated]; return MB_AUTORELEASE(hud); } + (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated { MBProgressHUD *hud = [self HUDForView:view]; if (hud != nil) { hud.removeFromSuperViewOnHide = YES; [hud hide:animated]; return YES; } return NO; } + (NSUInteger)hideAllHUDsForView:(UIView *)view animated:(BOOL)animated { NSArray *huds = [MBProgressHUD allHUDsForView:view]; for (MBProgressHUD *hud in huds) { hud.removeFromSuperViewOnHide = YES; [hud hide:animated]; } return [huds count]; } + (MB_INSTANCETYPE)HUDForView:(UIView *)view { NSEnumerator *subviewsEnum = [view.subviews reverseObjectEnumerator]; for (UIView *subview in subviewsEnum) { if ([subview isKindOfClass:self]) { return (MBProgressHUD *)subview; } } return nil; } + (NSArray *)allHUDsForView:(UIView *)view { NSMutableArray *huds = [NSMutableArray array]; NSArray *subviews = view.subviews; for (UIView *aView in subviews) { if ([aView isKindOfClass:self]) { [huds addObject:aView]; } } return [NSArray arrayWithArray:huds]; } #pragma mark - Lifecycle - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // Set default values for properties self.animationType = MBProgressHUDAnimationFade; self.mode = MBProgressHUDModeIndeterminate; self.labelText = nil; self.detailsLabelText = nil; self.opacity = 0.8f; self.color = nil; self.labelFont = [UIFont boldSystemFontOfSize:kLabelFontSize]; self.labelColor = [UIColor whiteColor]; self.detailsLabelFont = [UIFont boldSystemFontOfSize:kDetailsLabelFontSize]; self.detailsLabelColor = [UIColor whiteColor]; self.activityIndicatorColor = [UIColor whiteColor]; self.xOffset = 0.0f; self.yOffset = 0.0f; self.dimBackground = NO; self.margin = 20.0f; self.cornerRadius = 10.0f; self.graceTime = 0.0f; self.minShowTime = 0.0f; self.removeFromSuperViewOnHide = NO; self.minSize = CGSizeZero; self.square = NO; self.contentMode = UIViewContentModeCenter; self.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; // Transparent background self.opaque = NO; self.backgroundColor = [UIColor clearColor]; // Make it invisible for now self.alpha = 0.0f; taskInProgress = NO; rotationTransform = CGAffineTransformIdentity; [self setupLabels]; [self updateIndicators]; [self registerForKVO]; [self registerForNotifications]; } return self; } - (id)initWithView:(UIView *)view { NSAssert(view, @"View must not be nil."); return [self initWithFrame:view.bounds]; } - (id)initWithWindow:(UIWindow *)window { return [self initWithView:window]; } - (void)dealloc { [self unregisterFromNotifications]; [self unregisterFromKVO]; #if !__has_feature(objc_arc) [color release]; [indicator release]; [label release]; [detailsLabel release]; [labelText release]; [detailsLabelText release]; [graceTimer release]; [minShowTimer release]; [showStarted release]; [customView release]; [labelFont release]; [labelColor release]; [detailsLabelFont release]; [detailsLabelColor release]; #if NS_BLOCKS_AVAILABLE [completionBlock release]; #endif [super dealloc]; #endif } #pragma mark - Show & hide - (void)show:(BOOL)animated { NSAssert([NSThread isMainThread], @"MBProgressHUD needs to be accessed on the main thread."); useAnimation = animated; // If the grace time is set postpone the HUD display if (self.graceTime > 0.0) { NSTimer *newGraceTimer = [NSTimer timerWithTimeInterval:self.graceTime target:self selector:@selector(handleGraceTimer:) userInfo:nil repeats:NO]; [[NSRunLoop currentRunLoop] addTimer:newGraceTimer forMode:NSRunLoopCommonModes]; self.graceTimer = newGraceTimer; } // ... otherwise show the HUD imediately else { [self showUsingAnimation:useAnimation]; } } - (void)hide:(BOOL)animated { NSAssert([NSThread isMainThread], @"MBProgressHUD needs to be accessed on the main thread."); useAnimation = animated; // If the minShow time is set, calculate how long the hud was shown, // and pospone the hiding operation if necessary if (self.minShowTime > 0.0 && showStarted) { NSTimeInterval interv = [[NSDate date] timeIntervalSinceDate:showStarted]; if (interv < self.minShowTime) { self.minShowTimer = [NSTimer scheduledTimerWithTimeInterval:(self.minShowTime - interv) target:self selector:@selector(handleMinShowTimer:) userInfo:nil repeats:NO]; return; } } // ... otherwise hide the HUD immediately [self hideUsingAnimation:useAnimation]; } - (void)hide:(BOOL)animated afterDelay:(NSTimeInterval)delay { [self performSelector:@selector(hideDelayed:) withObject:[NSNumber numberWithBool:animated] afterDelay:delay]; } - (void)hideDelayed:(NSNumber *)animated { [self hide:[animated boolValue]]; } #pragma mark - Timer callbacks - (void)handleGraceTimer:(NSTimer *)theTimer { // Show the HUD only if the task is still running if (taskInProgress) { [self showUsingAnimation:useAnimation]; } } - (void)handleMinShowTimer:(NSTimer *)theTimer { [self hideUsingAnimation:useAnimation]; } #pragma mark - View Hierrarchy - (void)didMoveToSuperview { [self updateForCurrentOrientationAnimated:NO]; } #pragma mark - Internal show & hide operations - (void)showUsingAnimation:(BOOL)animated { // Cancel any scheduled hideDelayed: calls [NSObject cancelPreviousPerformRequestsWithTarget:self]; [self setNeedsDisplay]; if (animated && animationType == MBProgressHUDAnimationZoomIn) { self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(0.5f, 0.5f)); } else if (animated && animationType == MBProgressHUDAnimationZoomOut) { self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(1.5f, 1.5f)); } self.showStarted = [NSDate date]; // Fade in if (animated) { [UIView beginAnimations:nil context:NULL]; [UIView setAnimationDuration:0.30]; self.alpha = 1.0f; if (animationType == MBProgressHUDAnimationZoomIn || animationType == MBProgressHUDAnimationZoomOut) { self.transform = rotationTransform; } [UIView commitAnimations]; } else { self.alpha = 1.0f; } } - (void)hideUsingAnimation:(BOOL)animated { // Fade out if (animated && showStarted) { [UIView beginAnimations:nil context:NULL]; [UIView setAnimationDuration:0.30]; [UIView setAnimationDelegate:self]; [UIView setAnimationDidStopSelector:@selector(animationFinished:finished:context:)]; // 0.02 prevents the hud from passing through touches during the animation the hud will get completely hidden // in the done method if (animationType == MBProgressHUDAnimationZoomIn) { self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(1.5f, 1.5f)); } else if (animationType == MBProgressHUDAnimationZoomOut) { self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(0.5f, 0.5f)); } self.alpha = 0.02f; [UIView commitAnimations]; } else { self.alpha = 0.0f; [self done]; } self.showStarted = nil; } - (void)animationFinished:(NSString *)animationID finished:(BOOL)finished context:(void*)context { [self done]; } - (void)done { [NSObject cancelPreviousPerformRequestsWithTarget:self]; isFinished = YES; self.alpha = 0.0f; if (removeFromSuperViewOnHide) { [self removeFromSuperview]; } #if NS_BLOCKS_AVAILABLE if (self.completionBlock) { self.completionBlock(); self.completionBlock = NULL; } #endif if ([delegate respondsToSelector:@selector(hudWasHidden:)]) { [delegate performSelector:@selector(hudWasHidden:) withObject:self]; } } #pragma mark - Threading - (void)showWhileExecuting:(SEL)method onTarget:(id)target withObject:(id)object animated:(BOOL)animated { methodForExecution = method; targetForExecution = MB_RETAIN(target); objectForExecution = MB_RETAIN(object); // Launch execution in new thread self.taskInProgress = YES; [NSThread detachNewThreadSelector:@selector(launchExecution) toTarget:self withObject:nil]; // Show HUD view [self show:animated]; } #if NS_BLOCKS_AVAILABLE - (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block { dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); [self showAnimated:animated whileExecutingBlock:block onQueue:queue completionBlock:NULL]; } - (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block completionBlock:(void (^)())completion { dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); [self showAnimated:animated whileExecutingBlock:block onQueue:queue completionBlock:completion]; } - (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block onQueue:(dispatch_queue_t)queue { [self showAnimated:animated whileExecutingBlock:block onQueue:queue completionBlock:NULL]; } - (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block onQueue:(dispatch_queue_t)queue completionBlock:(MBProgressHUDCompletionBlock)completion { self.taskInProgress = YES; self.completionBlock = completion; dispatch_async(queue, ^(void) { block(); dispatch_async(dispatch_get_main_queue(), ^(void) { [self cleanUp]; }); }); [self show:animated]; } #endif - (void)launchExecution { @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" // Start executing the requested task [targetForExecution performSelector:methodForExecution withObject:objectForExecution]; #pragma clang diagnostic pop // Task completed, update view in main thread (note: view operations should // be done only in the main thread) [self performSelectorOnMainThread:@selector(cleanUp) withObject:nil waitUntilDone:NO]; } } - (void)cleanUp { taskInProgress = NO; #if !__has_feature(objc_arc) [targetForExecution release]; [objectForExecution release]; #else targetForExecution = nil; objectForExecution = nil; #endif [self hide:useAnimation]; } #pragma mark - UI - (void)setupLabels { label = [[UILabel alloc] initWithFrame:self.bounds]; label.adjustsFontSizeToFitWidth = NO; label.textAlignment = MBLabelAlignmentCenter; label.opaque = NO; label.backgroundColor = [UIColor clearColor]; label.textColor = self.labelColor; label.font = self.labelFont; label.text = self.labelText; [self addSubview:label]; detailsLabel = [[UILabel alloc] initWithFrame:self.bounds]; detailsLabel.font = self.detailsLabelFont; detailsLabel.adjustsFontSizeToFitWidth = NO; detailsLabel.textAlignment = MBLabelAlignmentCenter; detailsLabel.opaque = NO; detailsLabel.backgroundColor = [UIColor clearColor]; detailsLabel.textColor = self.detailsLabelColor; detailsLabel.numberOfLines = 0; detailsLabel.font = self.detailsLabelFont; detailsLabel.text = self.detailsLabelText; [self addSubview:detailsLabel]; } - (void)updateIndicators { BOOL isActivityIndicator = [indicator isKindOfClass:[UIActivityIndicatorView class]]; BOOL isRoundIndicator = [indicator isKindOfClass:[MBRoundProgressView class]]; if (mode == MBProgressHUDModeIndeterminate) { if (!isActivityIndicator) { // Update to indeterminate indicator [indicator removeFromSuperview]; self.indicator = MB_AUTORELEASE([[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]); [(UIActivityIndicatorView *)indicator startAnimating]; [self addSubview:indicator]; } #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 50000 [(UIActivityIndicatorView *)indicator setColor:self.activityIndicatorColor]; #endif } else if (mode == MBProgressHUDModeDeterminateHorizontalBar) { // Update to bar determinate indicator [indicator removeFromSuperview]; self.indicator = MB_AUTORELEASE([[MBBarProgressView alloc] init]); [self addSubview:indicator]; } else if (mode == MBProgressHUDModeDeterminate || mode == MBProgressHUDModeAnnularDeterminate) { if (!isRoundIndicator) { // Update to determinante indicator [indicator removeFromSuperview]; self.indicator = MB_AUTORELEASE([[MBRoundProgressView alloc] init]); [self addSubview:indicator]; } if (mode == MBProgressHUDModeAnnularDeterminate) { [(MBRoundProgressView *)indicator setAnnular:YES]; } [(MBRoundProgressView *)indicator setProgressTintColor:self.activityIndicatorColor]; [(MBRoundProgressView *)indicator setBackgroundTintColor:[self.activityIndicatorColor colorWithAlphaComponent:0.1f]]; } else if (mode == MBProgressHUDModeCustomView && customView != indicator) { // Update custom view indicator [indicator removeFromSuperview]; self.indicator = customView; [self addSubview:indicator]; } else if (mode == MBProgressHUDModeText) { [indicator removeFromSuperview]; self.indicator = nil; } } #pragma mark - Layout - (void)layoutSubviews { [super layoutSubviews]; // Entirely cover the parent view UIView *parent = self.superview; if (parent) { self.frame = parent.bounds; } CGRect bounds = self.bounds; // Determine the total width and height needed CGFloat maxWidth = bounds.size.width - 4 * margin; CGSize totalSize = CGSizeZero; CGRect indicatorF = indicator.bounds; indicatorF.size.width = MIN(indicatorF.size.width, maxWidth); totalSize.width = MAX(totalSize.width, indicatorF.size.width); totalSize.height += indicatorF.size.height; CGSize labelSize = MB_TEXTSIZE(label.text, label.font); labelSize.width = MIN(labelSize.width, maxWidth); totalSize.width = MAX(totalSize.width, labelSize.width); totalSize.height += labelSize.height; if (labelSize.height > 0.f && indicatorF.size.height > 0.f) { totalSize.height += kPadding; } CGFloat remainingHeight = bounds.size.height - totalSize.height - kPadding - 4 * margin; CGSize maxSize = CGSizeMake(maxWidth, remainingHeight); CGSize detailsLabelSize = MB_MULTILINE_TEXTSIZE(detailsLabel.text, detailsLabel.font, maxSize, detailsLabel.lineBreakMode); totalSize.width = MAX(totalSize.width, detailsLabelSize.width); totalSize.height += detailsLabelSize.height; if (detailsLabelSize.height > 0.f && (indicatorF.size.height > 0.f || labelSize.height > 0.f)) { totalSize.height += kPadding; } totalSize.width += 2 * margin; totalSize.height += 2 * margin; // Position elements CGFloat yPos = round(((bounds.size.height - totalSize.height) / 2)) + margin + yOffset; CGFloat xPos = xOffset; indicatorF.origin.y = yPos; indicatorF.origin.x = round((bounds.size.width - indicatorF.size.width) / 2) + xPos; indicator.frame = indicatorF; yPos += indicatorF.size.height; if (labelSize.height > 0.f && indicatorF.size.height > 0.f) { yPos += kPadding; } CGRect labelF; labelF.origin.y = yPos; labelF.origin.x = round((bounds.size.width - labelSize.width) / 2) + xPos; labelF.size = labelSize; label.frame = labelF; yPos += labelF.size.height; if (detailsLabelSize.height > 0.f && (indicatorF.size.height > 0.f || labelSize.height > 0.f)) { yPos += kPadding; } CGRect detailsLabelF; detailsLabelF.origin.y = yPos; detailsLabelF.origin.x = round((bounds.size.width - detailsLabelSize.width) / 2) + xPos; detailsLabelF.size = detailsLabelSize; detailsLabel.frame = detailsLabelF; // Enforce minsize and quare rules if (square) { CGFloat max = MAX(totalSize.width, totalSize.height); if (max <= bounds.size.width - 2 * margin) { totalSize.width = max; } if (max <= bounds.size.height - 2 * margin) { totalSize.height = max; } } if (totalSize.width < minSize.width) { totalSize.width = minSize.width; } if (totalSize.height < minSize.height) { totalSize.height = minSize.height; } size = totalSize; } #pragma mark BG Drawing - (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); UIGraphicsPushContext(context); if (self.dimBackground) { //Gradient colours size_t gradLocationsNum = 2; CGFloat gradLocations[2] = {0.0f, 1.0f}; CGFloat gradColors[8] = {0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.75f}; CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, gradColors, gradLocations, gradLocationsNum); CGColorSpaceRelease(colorSpace); //Gradient center CGPoint gradCenter= CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2); //Gradient radius float gradRadius = MIN(self.bounds.size.width , self.bounds.size.height) ; //Gradient draw CGContextDrawRadialGradient (context, gradient, gradCenter, 0, gradCenter, gradRadius, kCGGradientDrawsAfterEndLocation); CGGradientRelease(gradient); } // Set background rect color if (self.color) { CGContextSetFillColorWithColor(context, self.color.CGColor); } else { CGContextSetGrayFillColor(context, 0.0f, self.opacity); } // Center HUD CGRect allRect = self.bounds; // Draw rounded HUD backgroud rect CGRect boxRect = CGRectMake(round((allRect.size.width - size.width) / 2) + self.xOffset, round((allRect.size.height - size.height) / 2) + self.yOffset, size.width, size.height); float radius = self.cornerRadius; CGContextBeginPath(context); CGContextMoveToPoint(context, CGRectGetMinX(boxRect) + radius, CGRectGetMinY(boxRect)); CGContextAddArc(context, CGRectGetMaxX(boxRect) - radius, CGRectGetMinY(boxRect) + radius, radius, 3 * (float)M_PI / 2, 0, 0); CGContextAddArc(context, CGRectGetMaxX(boxRect) - radius, CGRectGetMaxY(boxRect) - radius, radius, 0, (float)M_PI / 2, 0); CGContextAddArc(context, CGRectGetMinX(boxRect) + radius, CGRectGetMaxY(boxRect) - radius, radius, (float)M_PI / 2, (float)M_PI, 0); CGContextAddArc(context, CGRectGetMinX(boxRect) + radius, CGRectGetMinY(boxRect) + radius, radius, (float)M_PI, 3 * (float)M_PI / 2, 0); CGContextClosePath(context); CGContextFillPath(context); UIGraphicsPopContext(); } #pragma mark - KVO - (void)registerForKVO { for (NSString *keyPath in [self observableKeypaths]) { [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL]; } } - (void)unregisterFromKVO { for (NSString *keyPath in [self observableKeypaths]) { [self removeObserver:self forKeyPath:keyPath]; } } - (NSArray *)observableKeypaths { return [NSArray arrayWithObjects:@"mode", @"customView", @"labelText", @"labelFont", @"labelColor", @"detailsLabelText", @"detailsLabelFont", @"detailsLabelColor", @"progress", @"activityIndicatorColor", nil]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (![NSThread isMainThread]) { [self performSelectorOnMainThread:@selector(updateUIForKeypath:) withObject:keyPath waitUntilDone:NO]; } else { [self updateUIForKeypath:keyPath]; } } - (void)updateUIForKeypath:(NSString *)keyPath { if ([keyPath isEqualToString:@"mode"] || [keyPath isEqualToString:@"customView"] || [keyPath isEqualToString:@"activityIndicatorColor"]) { [self updateIndicators]; } else if ([keyPath isEqualToString:@"labelText"]) { label.text = self.labelText; } else if ([keyPath isEqualToString:@"labelFont"]) { label.font = self.labelFont; } else if ([keyPath isEqualToString:@"labelColor"]) { label.textColor = self.labelColor; } else if ([keyPath isEqualToString:@"detailsLabelText"]) { detailsLabel.text = self.detailsLabelText; } else if ([keyPath isEqualToString:@"detailsLabelFont"]) { detailsLabel.font = self.detailsLabelFont; } else if ([keyPath isEqualToString:@"detailsLabelColor"]) { detailsLabel.textColor = self.detailsLabelColor; } else if ([keyPath isEqualToString:@"progress"]) { if ([indicator respondsToSelector:@selector(setProgress:)]) { [(id)indicator setValue:@(progress) forKey:@"progress"]; } return; } [self setNeedsLayout]; [self setNeedsDisplay]; } #pragma mark - Notifications - (void)registerForNotifications { #if !TARGET_OS_TV NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc addObserver:self selector:@selector(statusBarOrientationDidChange:) name:UIApplicationDidChangeStatusBarOrientationNotification object:nil]; #endif } - (void)unregisterFromNotifications { #if !TARGET_OS_TV NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc removeObserver:self name:UIApplicationDidChangeStatusBarOrientationNotification object:nil]; #endif } #if !TARGET_OS_TV - (void)statusBarOrientationDidChange:(NSNotification *)notification { UIView *superview = self.superview; if (!superview) { return; } else { [self updateForCurrentOrientationAnimated:YES]; } } #endif - (void)updateForCurrentOrientationAnimated:(BOOL)animated { // Stay in sync with the superview in any case if (self.superview) { self.bounds = self.superview.bounds; [self setNeedsDisplay]; } // Not needed on iOS 8+, compile out when the deployment target allows, // to avoid sharedApplication problems on extension targets #if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000 // Only needed pre iOS 7 when added to a window BOOL iOS8OrLater = kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_8_0; if (iOS8OrLater || ![self.superview isKindOfClass:[UIWindow class]]) return; UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; CGFloat radians = 0; if (UIInterfaceOrientationIsLandscape(orientation)) { if (orientation == UIInterfaceOrientationLandscapeLeft) { radians = -(CGFloat)M_PI_2; } else { radians = (CGFloat)M_PI_2; } // Window coordinates differ! self.bounds = CGRectMake(0, 0, self.bounds.size.height, self.bounds.size.width); } else { if (orientation == UIInterfaceOrientationPortraitUpsideDown) { radians = (CGFloat)M_PI; } else { radians = 0; } } rotationTransform = CGAffineTransformMakeRotation(radians); if (animated) { [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.3]; } [self setTransform:rotationTransform]; if (animated) { [UIView commitAnimations]; } #endif } @end @implementation MBRoundProgressView #pragma mark - Lifecycle - (id)init { return [self initWithFrame:CGRectMake(0.f, 0.f, 37.f, 37.f)]; } - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.backgroundColor = [UIColor clearColor]; self.opaque = NO; _progress = 0.f; _annular = NO; _progressTintColor = [[UIColor alloc] initWithWhite:1.f alpha:1.f]; _backgroundTintColor = [[UIColor alloc] initWithWhite:1.f alpha:.1f]; [self registerForKVO]; } return self; } - (void)dealloc { [self unregisterFromKVO]; #if !__has_feature(objc_arc) [_progressTintColor release]; [_backgroundTintColor release]; [super dealloc]; #endif } #pragma mark - Drawing - (void)drawRect:(CGRect)rect { CGRect allRect = self.bounds; CGRect circleRect = CGRectInset(allRect, 2.0f, 2.0f); CGContextRef context = UIGraphicsGetCurrentContext(); if (_annular) { // Draw background BOOL isPreiOS7 = kCFCoreFoundationVersionNumber < kCFCoreFoundationVersionNumber_iOS_7_0; CGFloat lineWidth = isPreiOS7 ? 5.f : 2.f; UIBezierPath *processBackgroundPath = [UIBezierPath bezierPath]; processBackgroundPath.lineWidth = lineWidth; processBackgroundPath.lineCapStyle = kCGLineCapButt; CGPoint center = CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2); CGFloat radius = (self.bounds.size.width - lineWidth)/2; CGFloat startAngle = - ((float)M_PI / 2); // 90 degrees CGFloat endAngle = (2 * (float)M_PI) + startAngle; [processBackgroundPath addArcWithCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES]; [_backgroundTintColor set]; [processBackgroundPath stroke]; // Draw progress UIBezierPath *processPath = [UIBezierPath bezierPath]; processPath.lineCapStyle = isPreiOS7 ? kCGLineCapRound : kCGLineCapSquare; processPath.lineWidth = lineWidth; endAngle = (self.progress * 2 * (float)M_PI) + startAngle; [processPath addArcWithCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES]; [_progressTintColor set]; [processPath stroke]; } else { // Draw background [_progressTintColor setStroke]; [_backgroundTintColor setFill]; CGContextSetLineWidth(context, 2.0f); CGContextFillEllipseInRect(context, circleRect); CGContextStrokeEllipseInRect(context, circleRect); // Draw progress CGPoint center = CGPointMake(allRect.size.width / 2, allRect.size.height / 2); CGFloat radius = (allRect.size.width - 4) / 2; CGFloat startAngle = - ((float)M_PI / 2); // 90 degrees CGFloat endAngle = (self.progress * 2 * (float)M_PI) + startAngle; [_progressTintColor setFill]; CGContextMoveToPoint(context, center.x, center.y); CGContextAddArc(context, center.x, center.y, radius, startAngle, endAngle, 0); CGContextClosePath(context); CGContextFillPath(context); } } #pragma mark - KVO - (void)registerForKVO { for (NSString *keyPath in [self observableKeypaths]) { [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL]; } } - (void)unregisterFromKVO { for (NSString *keyPath in [self observableKeypaths]) { [self removeObserver:self forKeyPath:keyPath]; } } - (NSArray *)observableKeypaths { return [NSArray arrayWithObjects:@"progressTintColor", @"backgroundTintColor", @"progress", @"annular", nil]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { [self setNeedsDisplay]; } @end @implementation MBBarProgressView #pragma mark - Lifecycle - (id)init { return [self initWithFrame:CGRectMake(.0f, .0f, 120.0f, 20.0f)]; } - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { _progress = 0.f; _lineColor = [UIColor whiteColor]; _progressColor = [UIColor whiteColor]; _progressRemainingColor = [UIColor clearColor]; self.backgroundColor = [UIColor clearColor]; self.opaque = NO; [self registerForKVO]; } return self; } - (void)dealloc { [self unregisterFromKVO]; #if !__has_feature(objc_arc) [_lineColor release]; [_progressColor release]; [_progressRemainingColor release]; [super dealloc]; #endif } #pragma mark - Drawing - (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetLineWidth(context, 2); CGContextSetStrokeColorWithColor(context,[_lineColor CGColor]); CGContextSetFillColorWithColor(context, [_progressRemainingColor CGColor]); // Draw background float radius = (rect.size.height / 2) - 2; CGContextMoveToPoint(context, 2, rect.size.height/2); CGContextAddArcToPoint(context, 2, 2, radius + 2, 2, radius); CGContextAddLineToPoint(context, rect.size.width - radius - 2, 2); CGContextAddArcToPoint(context, rect.size.width - 2, 2, rect.size.width - 2, rect.size.height / 2, radius); CGContextAddArcToPoint(context, rect.size.width - 2, rect.size.height - 2, rect.size.width - radius - 2, rect.size.height - 2, radius); CGContextAddLineToPoint(context, radius + 2, rect.size.height - 2); CGContextAddArcToPoint(context, 2, rect.size.height - 2, 2, rect.size.height/2, radius); CGContextFillPath(context); // Draw border CGContextMoveToPoint(context, 2, rect.size.height/2); CGContextAddArcToPoint(context, 2, 2, radius + 2, 2, radius); CGContextAddLineToPoint(context, rect.size.width - radius - 2, 2); CGContextAddArcToPoint(context, rect.size.width - 2, 2, rect.size.width - 2, rect.size.height / 2, radius); CGContextAddArcToPoint(context, rect.size.width - 2, rect.size.height - 2, rect.size.width - radius - 2, rect.size.height - 2, radius); CGContextAddLineToPoint(context, radius + 2, rect.size.height - 2); CGContextAddArcToPoint(context, 2, rect.size.height - 2, 2, rect.size.height/2, radius); CGContextStrokePath(context); CGContextSetFillColorWithColor(context, [_progressColor CGColor]); radius = radius - 2; float amount = self.progress * rect.size.width; // Progress in the middle area if (amount >= radius + 4 && amount <= (rect.size.width - radius - 4)) { CGContextMoveToPoint(context, 4, rect.size.height/2); CGContextAddArcToPoint(context, 4, 4, radius + 4, 4, radius); CGContextAddLineToPoint(context, amount, 4); CGContextAddLineToPoint(context, amount, radius + 4); CGContextMoveToPoint(context, 4, rect.size.height/2); CGContextAddArcToPoint(context, 4, rect.size.height - 4, radius + 4, rect.size.height - 4, radius); CGContextAddLineToPoint(context, amount, rect.size.height - 4); CGContextAddLineToPoint(context, amount, radius + 4); CGContextFillPath(context); } // Progress in the right arc else if (amount > radius + 4) { float x = amount - (rect.size.width - radius - 4); CGContextMoveToPoint(context, 4, rect.size.height/2); CGContextAddArcToPoint(context, 4, 4, radius + 4, 4, radius); CGContextAddLineToPoint(context, rect.size.width - radius - 4, 4); float angle = -acos(x/radius); if (isnan(angle)) angle = 0; CGContextAddArc(context, rect.size.width - radius - 4, rect.size.height/2, radius, M_PI, angle, 0); CGContextAddLineToPoint(context, amount, rect.size.height/2); CGContextMoveToPoint(context, 4, rect.size.height/2); CGContextAddArcToPoint(context, 4, rect.size.height - 4, radius + 4, rect.size.height - 4, radius); CGContextAddLineToPoint(context, rect.size.width - radius - 4, rect.size.height - 4); angle = acos(x/radius); if (isnan(angle)) angle = 0; CGContextAddArc(context, rect.size.width - radius - 4, rect.size.height/2, radius, -M_PI, angle, 1); CGContextAddLineToPoint(context, amount, rect.size.height/2); CGContextFillPath(context); } // Progress is in the left arc else if (amount < radius + 4 && amount > 0) { CGContextMoveToPoint(context, 4, rect.size.height/2); CGContextAddArcToPoint(context, 4, 4, radius + 4, 4, radius); CGContextAddLineToPoint(context, radius + 4, rect.size.height/2); CGContextMoveToPoint(context, 4, rect.size.height/2); CGContextAddArcToPoint(context, 4, rect.size.height - 4, radius + 4, rect.size.height - 4, radius); CGContextAddLineToPoint(context, radius + 4, rect.size.height/2); CGContextFillPath(context); } } #pragma mark - KVO - (void)registerForKVO { for (NSString *keyPath in [self observableKeypaths]) { [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL]; } } - (void)unregisterFromKVO { for (NSString *keyPath in [self observableKeypaths]) { [self removeObserver:self forKeyPath:keyPath]; } } - (NSArray *)observableKeypaths { return [NSArray arrayWithObjects:@"lineColor", @"progressRemainingColor", @"progressColor", @"progress", nil]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { [self setNeedsDisplay]; } @end