// // zhPopupController.m // // // Created by zhanghao on 2016/11/15. // Copyright © 2017年 snail-z. All rights reserved. // #import "zhPopupController.h" #import static NSMutableSet *gShowingZhPopupControllers; @interface zhPopupController () @property (nonatomic, strong, readonly) UIView *superview; @property (nonatomic, strong, readonly) UIView *maskView; @property (nonatomic, strong, readonly) UIView *contentView; @property (nonatomic, assign, readonly) CGFloat dropAngle; @property (nonatomic, assign, readonly) CGPoint markerCenter; @property (nonatomic, assign, readonly) zhPopupMaskType maskType; @end static void *zhPopupControllerParametersKey = &zhPopupControllerParametersKey; static void *zhPopupControllerNSTimerKey = &zhPopupControllerNSTimerKey; @implementation zhPopupController + (instancetype)popupControllerWithMaskType:(zhPopupMaskType)maskType { return [[self alloc] initWithMaskType:maskType]; } - (instancetype)init { return [self initWithMaskType:zhPopupMaskTypeBlackTranslucent]; } - (instancetype)initWithMaskType:(zhPopupMaskType)maskType { if (self = [super init]) { _isPresenting = NO; _maskType = maskType; _layoutType = zhPopupLayoutTypeCenter; _dismissOnMaskTouched = YES; _autoAdjustOffsetWhenKeyboardFrameChanged = YES; // setter self.maskAlpha = 0.5f; self.slideStyle = zhPopupSlideStyleFade; self.dismissOppositeDirection = NO; self.allowPan = NO; // superview _superview = [self frontWindow]; // maskView if (maskType == zhPopupMaskTypeBlackBlur || maskType == zhPopupMaskTypeWhiteBlur) { if ([[UIDevice currentDevice].systemVersion compare:@"8.0" options:NSNumericSearch] == NSOrderedAscending) { _maskView = [[UIToolbar alloc] initWithFrame:_superview.bounds]; } else { _maskView = [[UIView alloc] initWithFrame:_superview.bounds]; UIVisualEffectView *visualEffectView; visualEffectView = [[UIVisualEffectView alloc] init]; visualEffectView.effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]; visualEffectView.frame = _superview.bounds; [_maskView insertSubview:visualEffectView atIndex:0]; } } else { _maskView = [[UIView alloc] initWithFrame:_superview.bounds]; } switch (maskType) { case zhPopupMaskTypeBlackBlur: { if ([_maskView isKindOfClass:[UIToolbar class]]) { [(UIToolbar *)_maskView setBarStyle:UIBarStyleBlack]; } else { UIVisualEffectView *effectView = (UIVisualEffectView *)_maskView.subviews.firstObject; effectView.effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]; } } break; case zhPopupMaskTypeWhiteBlur: { if ([_maskView isKindOfClass:[UIToolbar class]]) { [(UIToolbar *)_maskView setBarStyle:UIBarStyleDefault]; } else { UIVisualEffectView *effectView = (UIVisualEffectView *)_maskView.subviews.firstObject; effectView.effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]; } } break; case zhPopupMaskTypeWhite: _maskView.backgroundColor = [UIColor whiteColor]; break; case zhPopupMaskTypeClear: _maskView.backgroundColor = [UIColor clearColor]; break; default: // zhPopupMaskTypeBlackTranslucent _maskView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:_maskAlpha]; break; } UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)]; tap.delegate = self; [_maskView addGestureRecognizer:tap]; // popupView _popupView = [[UIView alloc] init]; _popupView.backgroundColor = [UIColor clearColor]; // addSubview [_maskView addSubview:_popupView]; [_superview addSubview:_maskView]; // Observer statusBar orientation changes. [self bindNotificationEvent]; } return self; } #pragma mark - Setter - (void)setDismissOppositeDirection:(BOOL)dismissOppositeDirection { _dismissOppositeDirection = dismissOppositeDirection; objc_setAssociatedObject(self, _cmd, @(dismissOppositeDirection), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (void)setSlideStyle:(zhPopupSlideStyle)slideStyle { _slideStyle = slideStyle; objc_setAssociatedObject(self, _cmd, @(slideStyle), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (void)setMaskAlpha:(CGFloat)maskAlpha { if (_maskType != zhPopupMaskTypeBlackTranslucent) return; _maskAlpha = maskAlpha; _maskView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:_maskAlpha]; } - (void)setAllowPan:(BOOL)allowPan { if (!allowPan) return; if (_allowPan != allowPan) { _allowPan = allowPan; UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)]; [_popupView addGestureRecognizer:pan]; } } - (void)setAlpha:(CGFloat)alpha { _alpha = alpha; _maskView.alpha = alpha; } #pragma mark - Present - (void)presentContentView:(UIView *)contentView { [self presentContentView:contentView duration:0.25 springAnimated:NO]; } - (void)presentContentView:(UIView *)contentView displayTime:(NSTimeInterval)displayTime { [self presentContentView:contentView duration:0.25 springAnimated:NO inView:nil displayTime:displayTime]; } - (void)presentContentView:(UIView *)contentView duration:(NSTimeInterval)duration springAnimated:(BOOL)isSpringAnimated { [self presentContentView:contentView duration:duration springAnimated:isSpringAnimated inView:nil]; } - (void)presentContentView:(UIView *)contentView duration:(NSTimeInterval)duration springAnimated:(BOOL)isSpringAnimated inView:(UIView *)sView { [self presentContentView:contentView duration:duration springAnimated:isSpringAnimated inView:sView displayTime:0]; } - (void)presentContentView:(UIView *)contentView duration:(NSTimeInterval)duration springAnimated:(BOOL)isSpringAnimated inView:(UIView *)sView displayTime:(NSTimeInterval)displayTime { if (self.isPresenting) return; if (!gShowingZhPopupControllers) { gShowingZhPopupControllers = [NSMutableSet set]; } [gShowingZhPopupControllers addObject:self]; NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithCapacity:2]; [parameters setValue:@(duration) forKey:@"zh_duration"]; [parameters setValue:@(isSpringAnimated) forKey:@"zh_springAnimated"]; objc_setAssociatedObject(self, zhPopupControllerParametersKey, parameters, OBJC_ASSOCIATION_RETAIN_NONATOMIC); if (nil != self.willPresent) { self.willPresent(self); } else { if ([self.delegate respondsToSelector:@selector(popupControllerWillPresent:)]) { [self.delegate popupControllerWillPresent:self]; } } if (nil != sView) { _superview = sView; _maskView.frame = _superview.frame; } [self addContentView:contentView]; if (![_superview.subviews containsObject:_maskView]) { [_superview addSubview:_maskView]; } [self prepareDropAnimated]; [self prepareBackground]; _popupView.userInteractionEnabled = NO; _popupView.center = [self prepareCenter]; void (^presentCallback)(void) = ^() { self->_isPresenting = YES; self.popupView.userInteractionEnabled = YES; if (nil != self.didPresent) { self.didPresent(self); } else { if ([self.delegate respondsToSelector:@selector(popupControllerDidPresent:)]) { [self.delegate popupControllerDidPresent:self]; } } if (displayTime) { NSTimer *timer = [NSTimer timerWithTimeInterval:displayTime target:self selector:@selector(dismiss) userInfo:nil repeats:NO]; [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; objc_setAssociatedObject(self, zhPopupControllerNSTimerKey, timer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } }; if (isSpringAnimated) { [UIView animateWithDuration:duration delay:0.f usingSpringWithDamping:0.6 initialSpringVelocity:0.2 options:UIViewAnimationOptionCurveLinear animations:^{ [self finishedDropAnimated]; [self finishedBackground]; self.popupView.center = [self finishedCenter]; } completion:^(BOOL finished) { if (finished) presentCallback(); }]; } else { [UIView animateWithDuration:duration delay:0.f options:UIViewAnimationOptionCurveLinear animations:^{ [self finishedDropAnimated]; [self finishedBackground]; self.popupView.center = [self finishedCenter]; } completion:^(BOOL finished) { if (finished) presentCallback(); }]; } } #pragma mark - Dismiss - (void)fadeDismiss { objc_setAssociatedObject(self, _cmd, @(_slideStyle), OBJC_ASSOCIATION_RETAIN_NONATOMIC); _slideStyle = zhPopupSlideStyleFade; [self dismiss]; } - (void)dismiss { id object = objc_getAssociatedObject(self, zhPopupControllerParametersKey); if (object && [object isKindOfClass:[NSDictionary class]]) { NSTimeInterval duration = 0.0; NSNumber *durationNumber = [object valueForKey:@"zh_duration"]; if (nil != durationNumber) duration = durationNumber.doubleValue; BOOL flag = NO; NSNumber *flagNumber = [object valueForKey:@"zh_springAnimated"]; if (nil != flagNumber) flag = flagNumber.boolValue; [self dismissWithDuration:duration springAnimated:flag]; } } - (void)dismissWithDuration:(NSTimeInterval)duration springAnimated:(BOOL)isSpringAnimated { [self destroyTimer]; [gShowingZhPopupControllers removeObject:self]; // if (!self.isPresenting) return; if (nil != self.willDismiss) { self.willDismiss(self); } else { if ([self.delegate respondsToSelector:@selector(popupControllerWillDismiss:)]) { [self.delegate popupControllerWillDismiss:self]; } } void (^dismissCallback)(void) = ^() { self->_slideStyle = [objc_getAssociatedObject(self, @selector(fadeDismiss)) integerValue]; [self removeSubviews]; self->_isPresenting = NO; self.popupView.transform = CGAffineTransformIdentity; if (nil != self.didDismiss) { self.didDismiss(self); } else { if ([self.delegate respondsToSelector:@selector(popupControllerDidDismiss:)]) { [self.delegate popupControllerDidDismiss:self]; } } }; UIViewAnimationOptions (^animOpts)(zhPopupSlideStyle) = ^(zhPopupSlideStyle slide){ if (slide != zhPopupSlideStyleShrinkInOut1) { return UIViewAnimationOptionCurveLinear; } return UIViewAnimationOptionCurveEaseInOut; }; if (isSpringAnimated) { duration *= 0.75; NSTimeInterval duration1 = duration * 0.25, duration2 = duration - duration1; [UIView animateWithDuration:duration1 delay:0.f options:UIViewAnimationOptionCurveEaseInOut animations:^{ [self bufferBackground]; self.popupView.center = [self bufferCenter:30]; } completion:^(BOOL finished) { [UIView animateWithDuration:duration2 delay:0.f options:animOpts(self.slideStyle) animations:^{ [self dismissedDropAnimated]; [self dismissedBackground]; self.popupView.center = [self dismissedCenter]; } completion:^(BOOL finished) { if (finished) dismissCallback(); }]; }]; } else { [UIView animateWithDuration:duration delay:0.f options:animOpts(self.slideStyle) animations:^{ [self dismissedDropAnimated]; [self dismissedBackground]; self.popupView.center = [self dismissedCenter]; } completion:^(BOOL finished) { if (finished) dismissCallback(); }]; } } #pragma mark - Add contentView - (void)addContentView:(UIView *)contentView { if (!contentView) { if (nil != _popupView.superview) [_popupView removeFromSuperview]; return; } _contentView = contentView; if (_contentView.superview != _popupView) { _contentView.frame = (CGRect){.origin = CGPointZero, .size = contentView.frame.size}; _popupView.frame = _contentView.frame; _popupView.backgroundColor = _contentView.backgroundColor; if (_contentView.layer.cornerRadius) { _popupView.layer.cornerRadius = _contentView.layer.cornerRadius; _popupView.clipsToBounds = NO; } [_popupView addSubview:_contentView]; } } - (void)removeSubviews { if (_popupView.subviews.count > 0) { [_contentView removeFromSuperview]; _contentView = nil; } [_maskView removeFromSuperview]; } #pragma mark - Drop animated - (void)dropAnimatedWithRotateAngle:(CGFloat)angle { _dropAngle = angle; _slideStyle = zhPopupSlideStyleFromTop; } - (BOOL)dropSupport { return (_layoutType == zhPopupLayoutTypeCenter && _slideStyle == zhPopupSlideStyleFromTop); } static CGFloat zh_randomValue(int i, int j) { if (arc4random() % 2) return i; return j; } - (void)prepareDropAnimated { if (_dropAngle && [self dropSupport]) { _dismissOppositeDirection = YES; CGFloat ty = (_maskView.bounds.size.height + _popupView.frame.size.height) / 2; CATransform3D transform = CATransform3DMakeTranslation(0, -ty, 0); transform = CATransform3DRotate(transform, zh_randomValue(_dropAngle, -_dropAngle) * M_PI / 180, 0, 0, 1.0); _popupView.layer.transform = transform; } } - (void)finishedDropAnimated { if (_dropAngle && [self dropSupport]) { _popupView.layer.transform = CATransform3DIdentity; } } - (void)dismissedDropAnimated { if (_dropAngle && [self dropSupport]) { CGFloat ty = _maskView.bounds.size.height; CATransform3D transform = CATransform3DMakeTranslation(0, ty, 0); transform = CATransform3DRotate(transform, zh_randomValue(_dropAngle, -_dropAngle) * M_PI / 180, 0, 0, 1.0); _popupView.layer.transform = transform; } } #pragma mark - Mask view background - (void)prepareBackground { switch (_maskType) { case zhPopupMaskTypeBlackBlur: case zhPopupMaskTypeWhiteBlur: _maskView.alpha = 1; break; default: _maskView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0]; break; } } - (void)finishedBackground { switch (_maskType) { case zhPopupMaskTypeBlackTranslucent: _maskView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:_maskAlpha]; break; case zhPopupMaskTypeWhite: _maskView.backgroundColor = [UIColor whiteColor]; break; case zhPopupMaskTypeClear: _maskView.backgroundColor = [UIColor clearColor]; break; default: break; } } - (void)bufferBackground { switch (_maskType) { case zhPopupMaskTypeBlackBlur: case zhPopupMaskTypeWhiteBlur: break; case zhPopupMaskTypeBlackTranslucent: _maskView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:_maskAlpha - _maskAlpha * 0.15]; break; default: break; } } - (void)dismissedBackground { switch (_maskType) { case zhPopupMaskTypeBlackBlur: case zhPopupMaskTypeWhiteBlur: _maskView.alpha = 0; break; default: _maskView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0]; break; } } #pragma mark - Center point - (CGPoint)prepareCenterFrom:(NSInteger)type viewRef:(UIView *)viewRef{ switch (type) { case 0: // top return CGPointMake(viewRef.center.x, -_popupView.bounds.size.height / 2) ; case 1: // bottom return CGPointMake(viewRef.center.x, _maskView.bounds.size.height + _popupView.bounds.size.height / 2); case 2: // left return CGPointMake(-_popupView.bounds.size.width / 2, viewRef.center.y); case 3: // right return CGPointMake(_maskView.bounds.size.width + _popupView.bounds.size.width / 2, viewRef.center.y); default: // center return _maskView.center; } } - (CGPoint)prepareCenter { if (_layoutType == zhPopupLayoutTypeCenter) { CGPoint point = _maskView.center; if (_slideStyle == zhPopupSlideStyleShrinkInOut1) { _popupView.transform = CGAffineTransformMakeScale(0.15, 0.15); } else if (_slideStyle == zhPopupSlideStyleShrinkInOut2) { _popupView.transform = CGAffineTransformMakeScale(0.8, 0.8); } else if (_slideStyle == zhPopupSlideStyleFade) { _maskView.alpha = 0; } else { point = [self prepareCenterFrom:_slideStyle viewRef:_maskView]; } return point; } return [self prepareCenterFrom:_layoutType viewRef:_maskView]; } - (CGPoint)finishedCenter { CGPoint point = _maskView.center; switch (_layoutType) { case zhPopupLayoutTypeTop: return CGPointMake(point.x, _popupView.bounds.size.height / 2); case zhPopupLayoutTypeBottom: return CGPointMake(point.x, _maskView.bounds.size.height - _popupView.bounds.size.height / 2); case zhPopupLayoutTypeLeft: return CGPointMake(_popupView.bounds.size.width / 2, point.y); case zhPopupLayoutTypeRight: return CGPointMake(_maskView.bounds.size.width - _popupView.bounds.size.width / 2, point.y); default: // zhPopupLayoutTypeCenter { if (_slideStyle == zhPopupSlideStyleShrinkInOut1 || _slideStyle == zhPopupSlideStyleShrinkInOut2) { _popupView.transform = CGAffineTransformIdentity; } else if (_slideStyle == zhPopupSlideStyleFade) { _maskView.alpha = 1; } } return point; } } - (CGPoint)dismissedCenter { if (_layoutType != zhPopupLayoutTypeCenter) { return [self prepareCenterFrom:_layoutType viewRef:_popupView]; } switch (_slideStyle) { case zhPopupSlideStyleFromTop: return _dismissOppositeDirection ? CGPointMake(_popupView.center.x, _maskView.bounds.size.height + _popupView.bounds.size.height / 2) : CGPointMake(_popupView.center.x, -_popupView.bounds.size.height / 2); case zhPopupSlideStyleFromBottom: return _dismissOppositeDirection ? CGPointMake(_popupView.center.x, -_popupView.bounds.size.height / 2) : CGPointMake(_popupView.center.x, _maskView.bounds.size.height + _popupView.bounds.size.height / 2); case zhPopupSlideStyleFromLeft: return _dismissOppositeDirection ? CGPointMake(_maskView.bounds.size.width + _popupView.bounds.size.width / 2, _popupView.center.y) : CGPointMake(-_popupView.bounds.size.width / 2, _popupView.center.y); case zhPopupSlideStyleFromRight: return _dismissOppositeDirection ? CGPointMake(-_popupView.bounds.size.width / 2, _popupView.center.y) : CGPointMake(_maskView.bounds.size.width + _popupView.bounds.size.width / 2, _popupView.center.y); case zhPopupSlideStyleShrinkInOut1: _popupView.transform = _dismissOppositeDirection ? CGAffineTransformMakeScale(1.75, 1.75) : CGAffineTransformMakeScale(0.25, 0.25); break; case zhPopupSlideStyleShrinkInOut2: _popupView.transform = _dismissOppositeDirection ? CGAffineTransformMakeScale(1.2, 1.2) : CGAffineTransformMakeScale(0.75, 0.75); case zhPopupSlideStyleFade: _maskView.alpha = 0; default: break; } return _popupView.center; } #pragma mark - Buffer point - (CGPoint)bufferCenter:(CGFloat)move { CGPoint point = _popupView.center; switch (_layoutType) { case zhPopupLayoutTypeTop: point.y += move; break; case zhPopupLayoutTypeBottom: point.y -= move; break; case zhPopupLayoutTypeLeft: point.x += move; break; case zhPopupLayoutTypeRight: point.x -= move; break; case zhPopupLayoutTypeCenter: { switch (_slideStyle) { case zhPopupSlideStyleFromTop: point.y += _dismissOppositeDirection ? -move : move; break; case zhPopupSlideStyleFromBottom: point.y += _dismissOppositeDirection ? move : -move; break; case zhPopupSlideStyleFromLeft: point.x += _dismissOppositeDirection ? -move : move; break; case zhPopupSlideStyleFromRight: point.x += _dismissOppositeDirection ? move : -move; break; case zhPopupSlideStyleShrinkInOut1: case zhPopupSlideStyleShrinkInOut2: _popupView.transform = _dismissOppositeDirection ? CGAffineTransformMakeScale(0.95, 0.95) : CGAffineTransformMakeScale(1.05, 1.05); break; default: break; } } break; default: break; } return point; } #pragma mark - Destroy timer - (void)destroyTimer { id value = objc_getAssociatedObject(self, zhPopupControllerNSTimerKey); if (value) { [(NSTimer *)value invalidate]; objc_setAssociatedObject(self, zhPopupControllerNSTimerKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } } #pragma mark - FrontWindow - (UIWindow *)frontWindow { NSEnumerator *enumerator = [UIApplication.sharedApplication.windows reverseObjectEnumerator]; for (UIWindow *window in enumerator) { BOOL windowOnMainScreen = (window.screen == [UIScreen mainScreen]); BOOL windowIsVisible = !window.isHidden && window.alpha > 0; if (windowOnMainScreen && windowIsVisible && window.isKeyWindow) { return window; } } UIWindow *applicationWindow = [[UIApplication sharedApplication].delegate window]; if (!applicationWindow) NSLog(@"** zhPopupController ** Window is nil!"); return applicationWindow; } #pragma mark - Notifications - (void)bindNotificationEvent { [self unbindNotificationEvent]; [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willChangeStatusBarOrientation) name:UIApplicationWillChangeStatusBarOrientationNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didChangeStatusBarOrientation) name:UIApplicationDidChangeStatusBarOrientationNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil]; } - (void)unbindNotificationEvent { [[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications]; [[NSNotificationCenter defaultCenter]removeObserver:self name:UIApplicationWillChangeStatusBarOrientationNotification object:nil]; [[NSNotificationCenter defaultCenter]removeObserver:self name:UIApplicationDidChangeStatusBarOrientationNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillChangeFrameNotification object:nil]; } #pragma mark - Observing - (void)keyboardWillChangeFrame:(NSNotification*)notification { if (!_autoAdjustOffsetWhenKeyboardFrameChanged) return; _allowPan = NO; // The pan gesture will be invalid when the keyboard appears. NSDictionary *userInfo = notification.userInfo; CGRect keyboardRect = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; keyboardRect = [_maskView convertRect:keyboardRect fromView:nil]; CGFloat keyboardHeight = CGRectGetHeight(_maskView.bounds) - CGRectGetMinY(keyboardRect); UIViewAnimationCurve curve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue]; UIViewAnimationOptions options = curve << 16; NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; [UIView animateWithDuration:duration delay:0 options:options animations:^{ if (keyboardHeight > 0) { CGFloat offsetSpacing = self.offsetSpacingOfKeyboard, changeHeight = 0; switch (self.layoutType) { case zhPopupLayoutTypeTop: break; case zhPopupLayoutTypeBottom: changeHeight = keyboardHeight + offsetSpacing; break; default: changeHeight = (keyboardHeight / 2) + offsetSpacing; break; } if (!CGPointEqualToPoint(CGPointZero, self.markerCenter)) { self.popupView.center = CGPointMake(self.markerCenter.x, self.markerCenter.y - changeHeight); } else { self.popupView.center = CGPointMake(self.popupView.center.x, self.popupView.center.y - changeHeight); } } else { if (self.isPresenting) { self.popupView.center = [self finishedCenter]; } } } completion:^(BOOL finished) { self->_markerCenter = [self finishedCenter]; }]; } - (void)willChangeStatusBarOrientation { _maskView.frame = _superview.bounds; _popupView.center = [self finishedCenter]; [self dismiss]; } - (void)didChangeStatusBarOrientation { if ([[UIDevice currentDevice].systemVersion compare:@"8.0" options:NSNumericSearch] == NSOrderedAscending) { // must manually fix orientation prior to iOS 8 CGFloat angle; switch ([UIApplication sharedApplication].statusBarOrientation) { case UIInterfaceOrientationPortraitUpsideDown: angle = M_PI; break; case UIInterfaceOrientationLandscapeLeft: angle = -M_PI_2; break; case UIInterfaceOrientationLandscapeRight: angle = M_PI_2; break; default: // as UIInterfaceOrientationPortrait angle = 0.0; break; } _popupView.transform = CGAffineTransformMakeRotation(angle); } _maskView.frame = _superview.bounds; _popupView.center = [self finishedCenter]; } #pragma mark - Gesture Recognizer - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { if ([touch.view isDescendantOfView:_popupView]) return NO; return YES; } - (void)handleTap:(UITapGestureRecognizer *)g { if (_dismissOnMaskTouched) { if (!_dropAngle) { id object = objc_getAssociatedObject(self, @selector(setSlideStyle:)); _slideStyle = [object integerValue]; id obj = objc_getAssociatedObject(self, @selector(setDismissOppositeDirection:)); _dismissOppositeDirection = [obj boolValue]; } if (nil != self.maskTouched) { self.maskTouched(self); } else { [self dismiss]; } } } - (void)handlePan:(UIPanGestureRecognizer *)g { if (!_allowPan || !_isPresenting || _dropAngle) { return; } CGPoint translation = [g translationInView:_maskView]; switch (g.state) { case UIGestureRecognizerStateBegan: break; case UIGestureRecognizerStateChanged: { switch (_layoutType) { case zhPopupLayoutTypeCenter: { BOOL isTransformationVertical = NO; switch (_slideStyle) { case zhPopupSlideStyleFromLeft: case zhPopupSlideStyleFromRight: break; default: isTransformationVertical = YES; break; } NSInteger factor = 4; // set screen ratio `_maskView.bounds.size.height / factor` CGFloat changeValue; if (isTransformationVertical) { g.view.center = CGPointMake(g.view.center.x, g.view.center.y + translation.y); changeValue = g.view.center.y / (_maskView.bounds.size.height / factor); } else { g.view.center = CGPointMake(g.view.center.x + translation.x, g.view.center.y); changeValue = g.view.center.x / (_maskView.bounds.size.width / factor); } CGFloat alpha = factor / 2 - fabs(changeValue - factor / 2); [UIView animateWithDuration:0.15 animations:^{ self.maskView.alpha = alpha; } completion:NULL]; } break; case zhPopupLayoutTypeBottom: { if (g.view.frame.origin.y + translation.y > _maskView.bounds.size.height - g.view.bounds.size.height) { g.view.center = CGPointMake(g.view.center.x, g.view.center.y + translation.y); } } break; case zhPopupLayoutTypeTop: { if (g.view.frame.origin.y + g.view.frame.size.height + translation.y < g.view.bounds.size.height) { g.view.center = CGPointMake(g.view.center.x, g.view.center.y + translation.y); } } break; case zhPopupLayoutTypeLeft: { if (g.view.frame.origin.x + g.view.frame.size.width + translation.x < g.view.bounds.size.width) { g.view.center = CGPointMake(g.view.center.x + translation.x, g.view.center.y); } } break; case zhPopupLayoutTypeRight: { if (g.view.frame.origin.x + translation.x > _maskView.bounds.size.width - g.view.bounds.size.width) { g.view.center = CGPointMake(g.view.center.x + translation.x, g.view.center.y); } } break; default: break; } [g setTranslation:CGPointZero inView:_maskView]; } break; case UIGestureRecognizerStateEnded: { BOOL isWillDismiss = YES, isStyleCentered = NO; switch (_layoutType) { case zhPopupLayoutTypeCenter: { isStyleCentered = YES; if (g.view.center.y != _maskView.center.y) { if (g.view.center.y > _maskView.bounds.size.height * 0.25 && g.view.center.y < _maskView.bounds.size.height * 0.75) { isWillDismiss = NO; } } else { if (g.view.center.x > _maskView.bounds.size.width * 0.25 && g.view.center.x < _maskView.bounds.size.width * 0.75) { isWillDismiss = NO; } } } break; case zhPopupLayoutTypeBottom: isWillDismiss = g.view.frame.origin.y > _maskView.bounds.size.height - g.view.frame.size.height * 0.75; break; case zhPopupLayoutTypeTop: isWillDismiss = g.view.frame.origin.y + g.view.frame.size.height < g.view.frame.size.height * 0.75; break; case zhPopupLayoutTypeLeft: isWillDismiss = g.view.frame.origin.x + g.view.frame.size.width < g.view.frame.size.width * 0.75; break; case zhPopupLayoutTypeRight: isWillDismiss = g.view.frame.origin.x > _maskView.bounds.size.width - g.view.frame.size.width * 0.75; break; default: break; } if (isWillDismiss) { if (isStyleCentered) { switch (_slideStyle) { case zhPopupSlideStyleShrinkInOut1: case zhPopupSlideStyleShrinkInOut2: case zhPopupSlideStyleFade: { if (g.view.center.y < _maskView.bounds.size.height * 0.25) { _slideStyle = zhPopupSlideStyleFromTop; } else { if (g.view.center.y > _maskView.bounds.size.height * 0.75) { _slideStyle = zhPopupSlideStyleFromBottom; } } _dismissOppositeDirection = NO; } break; case zhPopupSlideStyleFromTop: _dismissOppositeDirection = !(g.view.center.y < _maskView.bounds.size.height * 0.25); break; case zhPopupSlideStyleFromBottom: _dismissOppositeDirection = g.view.center.y < _maskView.bounds.size.height * 0.25; break; case zhPopupSlideStyleFromLeft: _dismissOppositeDirection = !(g.view.center.x < _maskView.bounds.size.width * 0.25); break; case zhPopupSlideStyleFromRight: _dismissOppositeDirection = g.view.center.x < _maskView.bounds.size.width * 0.25; break; default: break; } } [self dismissWithDuration:0.25f springAnimated:NO]; } else { // restore view location. id object = objc_getAssociatedObject(self, zhPopupControllerParametersKey); NSNumber *flagNumber = [object valueForKey:@"zh_springAnimated"]; BOOL flag = NO; if (nil != flagNumber) { flag = flagNumber.boolValue; } NSTimeInterval duration = 0.25; NSNumber* durationNumber = [object valueForKey:@"zh_duration"]; if (nil != durationNumber) { duration = durationNumber.doubleValue; } if (flag) { [UIView animateWithDuration:duration delay:0.0 usingSpringWithDamping:0.6f initialSpringVelocity:0.2 options:UIViewAnimationOptionCurveLinear animations:^{ g.view.center = [self finishedCenter]; } completion:NULL]; } else { [UIView animateWithDuration:duration delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ g.view.center = [self finishedCenter]; } completion:NULL]; } } } break; case UIGestureRecognizerStateCancelled: break; default: break; } } - (void)dealloc { [self unbindNotificationEvent]; [self removeSubviews]; } @end @implementation UIViewController (zhPopupController) - (zhPopupController *)zh_popupController { id popupController = objc_getAssociatedObject(self, _cmd); if (nil == popupController) { popupController = [[zhPopupController alloc] init]; self.zh_popupController = popupController; } return popupController; } - (void)setZh_popupController:(zhPopupController *)zh_popupController { objc_setAssociatedObject(self, @selector(zh_popupController), zh_popupController, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } @end