// // MMProgressHUD.m // MMProgressHUD // // Created by Lars Anderson on 10/7/11. // Copyright 2011 Mutual Mobile. All rights reserved. // #import #import "MMProgressHUD.h" #import "MMProgressHUDCommon.h" #import "MMProgressHUD+Animations.h" #import "MMProgressHUDWindow.h" #import "MMProgressHUDViewController.h" #import "MMProgressHUDOverlayView.h" #import "MMVectorImage.h" #import "MMLinearProgressView.h" #import "MMRadialProgressView.h" #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_5_0 #error MMProgressHUD uses APIs only available in iOS 5.0+ #endif NSString * const MMProgressHUDDefaultConfirmationMessage = @"Cancel?"; NSString * const MMProgressHUDAnimationShow = @"mm-progress-hud-present-animation"; NSString * const MMProgressHUDAnimationDismiss = @"mm-progress-hud-dismiss-animation"; NSString * const MMProgressHUDAnimationWindowFadeOut = @"mm-progress-hud-window-fade-out"; NSString * const MMProgressHUDAnimationKeyShowAnimation = @"show"; NSString * const MMProgressHUDAnimationKeyDismissAnimation = @"dismiss"; NSUInteger const MMProgressHUDConfirmationPulseCount = 8;//Keep this number even CGFloat const MMProgressHUDStandardDismissDelay = 0.75f; CGSize const MMProgressHUDDefaultImageSize = {37.f, 37.f}; #pragma mark - MMProgressHUD @interface MMProgressHUD () @property (nonatomic, strong) MMProgressHUDWindow *window; @property (nonatomic, readwrite, getter = isVisible) BOOL visible; @property (nonatomic, copy) NSString *title; @property (nonatomic, copy) NSString *status; @property (nonatomic, strong) UIImage *image; @property (nonatomic, copy) NSArray *animationImages; @property (nonatomic, strong) CAAnimation *queuedShowAnimation; @property (nonatomic, strong) CAAnimation *queuedDismissAnimation; @property (nonatomic, readwrite, strong) MMProgressHUDOverlayView *overlayView; @property (nonatomic, strong) NSTimer *dismissDelayTimer; @property (nonatomic, copy) NSString *tempStatus; @property (nonatomic, strong) NSTimer *confirmationTimer; @property (nonatomic, getter = isConfirmed) BOOL confirmed; @property (nonatomic, assign) BOOL presentedAnimated; @property (nonatomic, strong) MMProgressHUDViewController *presentationViewController; @end @implementation MMProgressHUD #pragma mark - Class Methods + (instancetype)sharedHUD { static MMProgressHUD *__sharedHUD = nil; static dispatch_once_t mmSharedHUDOnceToken; dispatch_once(&mmSharedHUDOnceToken, ^{ __sharedHUD = [[MMProgressHUD alloc] init]; }); return __sharedHUD; } #pragma mark - Instance Presentation Methods - (void)showDeterminateProgressWithTitle:(NSString *)title status:(NSString *)status confirmationMessage:(NSString *)confirmation cancelBlock:(void (^)(void))cancelBlock images:(NSArray *)images { [self.hud setIndeterminate:NO]; [self showWithTitle:title status:status confirmationMessage:confirmation cancelBlock:cancelBlock images:images]; } - (void)showWithTitle:(NSString *)title status:(NSString *)status confirmationMessage:(NSString *)confirmationMessage cancelBlock:(void(^)(void))cancelBlock images:(NSArray *)images { self.image = nil; self.animationImages = nil; if (images.count == 1) { self.image = images[0]; } else if (images.count > 0) { self.animationImages = images; } else if (self.spinAnimationImages.count == 1) { self.image = self.spinAnimationImages[0]; } else if (self.spinAnimationImages.count > 0) { self.animationImages = self.spinAnimationImages; } self.cancelBlock = cancelBlock; self.title = title; self.status = status; if (confirmationMessage.length > 0) { self.confirmationMessage = confirmationMessage; } else { self.confirmationMessage = MMProgressHUDDefaultConfirmationMessage; } if ((self.isVisible == YES) && (self.window != nil) && ([self.hud.layer animationForKey:MMProgressHUDAnimationKeyDismissAnimation] == nil)) { [self _updateHUDAnimated:YES withCompletion:nil]; } else { [self show]; } } - (void)dismissWithCompletionState:(MMProgressHUDCompletionState)completionState title:(NSString *)title status:(NSString *)status afterDelay:(NSTimeInterval)delay { if (title) { self.title = title; } if (status) { self.status = status; } self.hud.completionState = completionState; if (self.isVisible) { [self _updateHUDAnimated:YES withCompletion:^(BOOL completed) { [self dismissAfterDelay:delay]; }]; } } - (void)updateProgress:(CGFloat)progress withStatus:(NSString *)status title:(NSString *)title{ [self setProgress:progress]; if (status != nil) { self.hud.messageText = status; } if (title != nil) { self.hud.titleText = title; } if (self.isVisible && (self.window != nil)) { void(^animationCompletion)(BOOL completed) = ^(BOOL completed) { if (progress >= 1.f && self.progressCompletion != nil) { double delayInSeconds = 0.33f;//allow enough time for progress to animate dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC); dispatch_after(popTime, dispatch_get_main_queue(), ^(void) { if (self.progressCompletion) { self.progressCompletion(); } }); } }; [self _updateHUDAnimated:YES withCompletion:animationCompletion]; } else { [self show]; } } #pragma mark - Initializers - (instancetype)init { if ( (self = [super initWithFrame:CGRectZero]) ) { self.hud = [[MMHud alloc] init]; self.hud.delegate = self; UIColor *imageFill = [UIColor colorWithWhite:1.f alpha:1.f]; self.errorImage = [MMVectorImage vectorImageShapeOfType:MMVectorShapeTypeX size:MMProgressHUDDefaultImageSize fillColor:imageFill]; self.successImage = [MMVectorImage vectorImageShapeOfType:MMVectorShapeTypeCheck size:MMProgressHUDDefaultImageSize fillColor:imageFill]; [self setAutoresizingMask: UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth]; } return self; } - (void)dealloc { MMHudLog(@"dealloc"); if (_window != nil) { [_window setHidden:YES]; } } - (void)forceCleanup { //Do not invoke this method unless you are in a unit test environment if (self.window != nil) { [self.window setHidden:YES]; } self.presentationViewController = nil; self.window.rootViewController = nil; self.window = nil; } #pragma mark - Passthrough Properties - (void)setOverlayMode:(MMProgressHUDWindowOverlayMode)overlayMode { self.overlayView.overlayMode = overlayMode; } - (MMProgressHUDWindowOverlayMode)overlayMode { return self.overlayView.overlayMode; } - (void)setAnimationLoopDuration:(CGFloat)animationLoopDuration { self.hud.animationLoopDuration = animationLoopDuration; } - (CGFloat)animationLoopDuration { return self.hud.animationLoopDuration; } - (void)setProgress:(CGFloat)progress { [self.hud setProgress:progress animated:YES]; self.hud.accessibilityValue = [NSString stringWithFormat:@"%i%%", (int)(progress/1.f*100)]; UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, [NSString stringWithFormat:@"%@ %@", self.hud.accessibilityLabel, self.hud.accessibilityValue]); } - (CGFloat)progress { return self.hud.progress; } - (void)setProgressStyle:(MMProgressHUDProgressStyle)progressStyle{ self.hud.progressStyle = progressStyle; switch (progressStyle) { case MMProgressHUDProgressStyleIndeterminate: self.hud.progressViewClass = nil; self.accessibilityTraits &= ~UIAccessibilityTraitUpdatesFrequently; break; case MMProgressHUDProgressStyleLinear: self.hud.progressViewClass = [MMLinearProgressView class]; self.accessibilityTraits |= UIAccessibilityTraitUpdatesFrequently; break; case MMProgressHUDProgressStyleRadial: self.hud.progressViewClass = [MMRadialProgressView class]; self.accessibilityTraits |= UIAccessibilityTraitUpdatesFrequently; break; } } - (MMProgressHUDProgressStyle)progressStyle{ return self.hud.progressStyle; } - (void)setTitle:(NSString *)title { self.hud.titleText = title; } - (NSString *)title { return self.hud.titleText; } - (void)setStatus:(NSString *)status { self.hud.messageText = status; } - (NSString *)status { return self.hud.messageText; } - (void)setImage:(UIImage *)image{ _image = image; [self.hud setImage:image]; } #pragma mark - Property Overrides - (void)setProgressViewClass:(Class)progressViewClass{ self.hud.progressViewClass = progressViewClass; } - (Class)progressViewClass{ return self.hud.progressViewClass; } - (MMProgressHUDOverlayView *)overlayView { if (_overlayView == nil) { _overlayView = [[MMProgressHUDOverlayView alloc] init]; _overlayView.alpha = 0.f; _overlayView.autoresizingMask = (UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth); } return _overlayView; } - (CGColorRef)glowColor { if (_glowColor == NULL) { CGColorRef redColor = CGColorRetain([UIColor redColor].CGColor); self.glowColor = redColor; CGColorRelease(redColor); } return _glowColor; } - (MMHud *)hud { if (_hud == nil) { _hud = [[MMHud alloc] init]; } return _hud; } - (void)setProgressCompletion:(void (^)(void))progressCompletion { if (progressCompletion != nil) { __typeof(self) __weak weakSelf = self; _progressCompletion = ^(void) { progressCompletion(); weakSelf.progressCompletion = nil; }; } else { _progressCompletion = nil; } } - (void)setCancelBlock:(void (^)(void))cancelBlock { _cancelBlock = cancelBlock; if (cancelBlock != nil) { self.hud.accessibilityTraits |= (UIAccessibilityTraitAllowsDirectInteraction | UIAccessibilityTraitButton); } else { self.hud.accessibilityTraits &= ~(UIAccessibilityTraitAllowsDirectInteraction | UIAccessibilityTraitButton); } } #pragma mark - Builders - (void)_buildHUDWindow { if (self.window == nil) { self.window = [[MMProgressHUDWindow alloc] init]; if (self.presentationViewController == nil) { self.presentationViewController = [[MMProgressHUDViewController alloc] init]; if (self.presentationViewController.view != self) [self.presentationViewController setView:self]; } [self.window setRootViewController:self.presentationViewController]; [self _buildOverlayViewForMode:self.overlayMode inView:self.window]; [self.window setHidden:NO]; } } - (void)_buildOverlayViewForMode:(MMProgressHUDWindowOverlayMode)overlayMode inView:(UIView *)view { self.overlayView.frame = view.bounds; self.overlayView.overlayMode = overlayMode; [view insertSubview:self.overlayView atIndex:0]; } - (void)_buildHUD { [self setAutoresizingMask: UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth]; [self _buildHUDWindow]; UITapGestureRecognizer *tapToDismiss = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_handleTap:)]; [tapToDismiss setNumberOfTapsRequired:1]; [tapToDismiss setNumberOfTouchesRequired:1]; [self addGestureRecognizer:tapToDismiss]; self.hud.image = self.image; self.hud.animationImages = self.animationImages; self.hud.layer.transform = CATransform3DIdentity; [self.hud setNeedsUpdate:YES]; [self.hud applyLayoutFrames]; [self addSubview:self.hud]; } #pragma mark - Layout - (void)_updateMessageLabelsAnimated:(BOOL)animated { [self.hud updateTitle:self.title message:self.status animated:animated]; } - (void)_updateHUDAnimated:(BOOL)animated withCompletion:(void(^)(BOOL completed))completionBlock { MMHudLog(@"Updating %@ with completion...", NSStringFromClass(self.class)); [self killDismissDelayTimer]; if (animated) { [UIView animateWithDuration:0.1f delay:0.f options:UIViewAnimationOptionCurveEaseOut | UIViewAnimationOptionBeginFromCurrentState animations:^{ [self _updateHUD]; } completion:completionBlock]; } else { [self _updateHUD]; if (completionBlock != nil) { completionBlock(YES); } } } - (void)_updateHUD { [self.hud updateLayoutFrames]; [self.hud updateAnimated:YES withCompletion:nil]; self.hud.center = [self _windowCenterForHUDAnchor:self.hud.layer.anchorPoint]; } - (void)killDismissDelayTimer { if ([self.dismissDelayTimer isValid]) { [self.dismissDelayTimer invalidate]; } self.dismissDelayTimer = nil; } - (CGPoint)_windowCenterForHUDAnchor:(CGPoint)anchor { CGFloat hudHeight = CGRectGetHeight(self.hud.frame); CGPoint windowCenter = self.window.center; CGFloat x = roundf(windowCenter.x); CGFloat y = roundf(windowCenter.y + (anchor.y - 0.5f) * hudHeight); UIInterfaceOrientation currentOrientation = [[self.window rootViewController] interfaceOrientation]; NSString *reqSysVer = @"8.0"; NSString *currSysVer = [[UIDevice currentDevice] systemVersion]; BOOL usesWindowTransformRotation = ([currSysVer compare:reqSysVer options:NSNumericSearch] != NSOrderedAscending); if (usesWindowTransformRotation == NO) { if (UIInterfaceOrientationIsPortrait(currentOrientation)) { y = roundf(windowCenter.y + (anchor.y - 0.5f) * hudHeight); x = roundf(windowCenter.x); } else { x = roundf(windowCenter.y); y = roundf(windowCenter.x + (anchor.y - 0.5f) * hudHeight); } } CGPoint position = CGPointMake(x, y); return [self _antialiasedPositionPointForPoint:position forLayer:self.hud.layer]; } #pragma mark - Presentation - (void)show { [self killDismissDelayTimer]; NSAssert([NSThread isMainThread], @"Show should be run on main thread!"); [CATransaction begin]; [CATransaction setDisableActions:YES]; [self _buildHUD]; self.presentedAnimated = YES; switch (self.presentationStyle) { case MMProgressHUDPresentationStyleDrop: [self _showWithDropAnimation]; break; case MMProgressHUDPresentationStyleExpand: [self _showWithExpandAnimation]; break; case MMProgressHUDPresentationStyleShrink: [self _showWithShrinkAnimation]; break; case MMProgressHUDPresentationStyleSwingLeft: [self _showWithSwingInAnimationFromLeft:YES]; break; case MMProgressHUDPresentationStyleSwingRight: [self _showWithSwingInAnimationFromLeft:NO]; break; case MMProgressHUDPresentationStyleBalloon: [self _showWithBalloonAnimation]; break; case MMProgressHUDPresentationStyleFade: [self _showWithFadeAnimation]; break; case MMProgressHUDPresentationStyleNone: default:{ self.presentedAnimated = NO; CGPoint newCenter = [self _windowCenterForHUDAnchor:self.hud.layer.anchorPoint]; self.hud.center = newCenter; self.hud.layer.transform = CATransform3DIdentity; self.hud.alpha = 1.f; self.overlayView.alpha = 1.0f; self.visible = YES; } break; } [CATransaction commit]; CGFloat duration = (self.presentationStyle == MMProgressHUDPresentationStyleNone) ? 0.f : MMProgressHUDAnimateInDurationShort; [UIView animateWithDuration:duration delay:0.f options:UIViewAnimationOptionCurveEaseOut | UIViewAnimationOptionBeginFromCurrentState animations:^{ self.overlayView.alpha = 1.0f; } completion:^(BOOL completed) { UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, self.hud.accessibilityLabel); }]; } - (void)dismissAfterDelay:(NSTimeInterval)delay { if (self.visible == NO) { MMHudLog(@"Preventing delayed dismissal when already dismissed!"); return; } [self killDismissDelayTimer]; self.dismissDelayTimer = [NSTimer scheduledTimerWithTimeInterval:delay target:self selector:@selector(dismiss) userInfo:nil repeats:NO]; } - (void)dismiss { if (self.visible == NO) { MMHudLog(@"Preventing dismissal when already dismissed!"); return; } NSAssert([NSThread isMainThread], @"Dismiss method should be run on main thread!"); MMHudLog(@"Dismissing..."); switch (self.presentationStyle) { case MMProgressHUDPresentationStyleDrop: [self _dismissWithDropAnimation]; break; case MMProgressHUDPresentationStyleExpand: [self _dismissWithExpandAnimation]; break; case MMProgressHUDPresentationStyleShrink: [self _dismissWithShrinkAnimation]; break; case MMProgressHUDPresentationStyleSwingLeft: [self _dismissWithSwingLeftAnimation]; break; case MMProgressHUDPresentationStyleSwingRight: [self _dismissWithSwingRightAnimation]; break; case MMProgressHUDPresentationStyleBalloon: [self _dismissWithBalloonAnimation]; break; case MMProgressHUDPresentationStyleFade: [self _dismissWithFadeAnimation]; break; case MMProgressHUDPresentationStyleNone: default: self.hud.layer.opacity = 0.f; self.overlayView.layer.opacity = 0.f; [self removeFromSuperview]; self.visible = NO; [self.window setHidden:YES]; self.window = nil; break; } typeof(self) __weak weakSelf = self; if (!self.queuedDismissAnimation) { [self _fadeOutAndCleanUp]; } else { void (^oldCompletion)(void) = [self.showAnimationCompletion copy]; self.showAnimationCompletion = ^{ [weakSelf _fadeOutAndCleanUp]; if (oldCompletion) oldCompletion(); }; } } - (void)_fadeOutAndCleanUp { NSTimeInterval duration = (self.presentationStyle == MMProgressHUDPresentationStyleNone) ? 0.0 : MMProgressHUDAnimateOutDurationLong; NSTimeInterval delay = (self.presentationStyle == MMProgressHUDPresentationStyleDrop) ? MMProgressHUDAnimateOutDurationShort : 0.0; [UIView animateWithDuration:duration delay:delay options:UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionBeginFromCurrentState animations:^{ self.overlayView.alpha = 0.f; } completion:^(BOOL finished) { self.image = nil; self.animationImages = nil; self.progress = 0.f; self.hud.completionState = MMProgressHUDCompletionStateNone; [self.presentationViewController removeFromParentViewController]; [self removeFromSuperview]; self.presentationViewController.view = nil; self.presentationViewController = nil; [self.window setHidden:YES], self.window = nil; self.cancelled = NO; UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil); }]; } - (CGPoint)_antialiasedPositionPointForPoint:(CGPoint)oldCenter forLayer:(CALayer *)layer { CGPoint newCenter = oldCenter; CGSize viewSize = layer.bounds.size; CGPoint anchor = layer.anchorPoint; double intPart; CGFloat viewXRemainder = modf(viewSize.width/2,&intPart); CGFloat viewCenterXRemainder = modf(oldCenter.x/2, &intPart); if (anchor.x != 0.f && anchor.x != 1.f) { if (((viewXRemainder == 0) &&//if view width is even (viewCenterXRemainder != 0)) ||//and if center x is odd ((viewXRemainder != 0) &&//if view width is odd (viewCenterXRemainder == 0))) {//and if center x is even newCenter.x = oldCenter.x + viewXRemainder; } } CGFloat viewYRemainder = modf(viewSize.height/2,&intPart); CGFloat viewCenterYRemainder = modf(oldCenter.y/2, &intPart); if (anchor.y != 0.f && anchor.y != 1.f) { if (((viewYRemainder == 0) &&//if view width is even (viewCenterYRemainder != 0)) ||//and if center x is odd ((viewYRemainder != 0) &&//if view width is odd (viewCenterYRemainder == 0))) {//and if center x is even newCenter.y = oldCenter.y + viewYRemainder; } } return newCenter; } #pragma mark - Gestures - (void)_handleTap:(UITapGestureRecognizer *)recognizer { MMHudLog(@"Handling tap"); if ((self.cancelBlock != nil) && (self.confirmed == NO)) { MMHudLog(@"Asking to confirm cancel"); self.confirmed = YES; self.tempStatus = [self.status copy]; CGFloat timerDuration = MMProgressHUDAnimateInDurationNormal*MMProgressHUDConfirmationPulseCount; self.confirmationTimer = [NSTimer scheduledTimerWithTimeInterval:timerDuration target:self selector:@selector(_resetConfirmationTimer:) userInfo:nil repeats:NO]; self.status = self.confirmationMessage; [self.hud updateTitle:self.hud.titleText message:self.confirmationMessage animated:YES]; [self _beginGlowAnimation]; } else if (self.confirmed) { self.cancelled = YES; MMHudLog(@"confirmed to dismiss!"); [self.confirmationTimer invalidate], self.confirmationTimer = nil; if (self.cancelBlock != nil) { self.cancelBlock(); } self.hud.completionState = MMProgressHUDCompletionStateError; [self.hud setNeedsUpdate:YES]; [self.hud updateAnimated:YES withCompletion:^(__unused BOOL completed) { [self dismiss]; }]; self.confirmed = NO; } } - (void)_resetConfirmationTimer:(NSTimer *)timer { MMHudLog(@"Resetting confirmation timer"); [self.confirmationTimer invalidate], self.confirmationTimer = nil; self.status = self.tempStatus; self.tempStatus = nil; self.confirmed = NO; [self _endGlowAnimation]; [self.hud updateTitle:self.hud.titleText message:self.status animated:YES]; } - (UIImage *)_imageForCompletionState:(MMProgressHUDCompletionState)completionState { switch (completionState) { case MMProgressHUDCompletionStateError: return self.errorImage; case MMProgressHUDCompletionStateSuccess: return self.successImage; case MMProgressHUDCompletionStateNone: return nil; } } #pragma mark - MMHud Delegate - (void)hudDidCompleteProgress:(MMHud *)hud { if (self.progressCompletion != nil) { self.progressCompletion(); } self.hud.accessibilityValue = nil; } - (UIImage *)hud:(MMHud *)hud imageForCompletionState:(MMProgressHUDCompletionState)completionState { return [self _imageForCompletionState:completionState]; } - (CGPoint)hudCenterPointForDisplay:(MMHud *)hud { return [self _windowCenterForHUDAnchor:hud.layer.anchorPoint]; } @end