#import "RCCNotification.h" #import "RCTRootView.h" #import "RCTHelpers.h" @interface NotificationView : UIView @property (nonatomic, strong) RCTRootView *reactView; @property (nonatomic, strong) UIView *slideAnimGapView; @property (nonatomic, strong) NSDictionary *params; @property (nonatomic, strong) NSTimer *autoDismissTimer; @property (nonatomic) BOOL yellowBoxRemoved; @end @implementation NotificationView -(id)initWithParams:(NSDictionary*)params { self = [super initWithFrame:CGRectZero]; if (self) { self.params = params; self.yellowBoxRemoved = NO; self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; self.reactView = [[RCTRootView alloc] initWithBridge:[[RCCManager sharedInstance] getBridge] moduleName:params[@"component"] initialProperties:params[@"passProps"]]; self.reactView.backgroundColor = [UIColor clearColor]; self.reactView.sizeFlexibility = RCTRootViewSizeFlexibilityWidthAndHeight; self.reactView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; [self addSubview:self.reactView]; [self.reactView.contentView.layer addObserver:self forKeyPath:@"frame" options:0 context:nil]; [self.reactView.contentView.layer addObserver:self forKeyPath:@"bounds" options:0 context:NULL]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onRNReload) name:RCTReloadNotification object:nil]; if ([params[@"dismissWithSwipe"] boolValue]) { UISwipeGestureRecognizer *swipeGesture = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(performDismiss)]; swipeGesture.direction = [self swipeDirection]; [self addGestureRecognizer:swipeGesture]; } if (params[@"shadowRadius"] != nil && [params[@"shadowRadius"] floatValue] > 0) { self.layer.shadowColor = [UIColor blackColor].CGColor; self.layer.shadowOffset = CGSizeMake(0, 0); self.layer.shadowRadius = [params[@"shadowRadius"] floatValue]; self.layer.shadowOpacity = 0.6; } self.hidden = YES; } return self; } -(void)layoutSubviews { [super layoutSubviews]; if(!self.yellowBoxRemoved) { self.yellowBoxRemoved = [RCTHelpers removeYellowBox:self.reactView]; } } -(UISwipeGestureRecognizerDirection)swipeDirection { UISwipeGestureRecognizerDirection direction = UISwipeGestureRecognizerDirectionUp; NSString *animationType = [self.params valueForKeyPath:@"animation.type"]; if ([animationType isEqualToString:@"swing"] || [animationType isEqualToString:@"slide-down"]) direction = UISwipeGestureRecognizerDirectionUp; else if ([animationType isEqualToString:@"slide-left"]) direction = UISwipeGestureRecognizerDirectionRight; else if ([animationType isEqualToString:@"slide-right"]) direction = UISwipeGestureRecognizerDirectionLeft; return direction; } -(void)killAutoDismissTimer { if (self.autoDismissTimer != nil) { [self.autoDismissTimer invalidate]; self.autoDismissTimer = nil; } } -(void)removeObservers { [[NSNotificationCenter defaultCenter] removeObserver:self]; [self.reactView.contentView.layer removeObserver:self forKeyPath:@"frame" context:nil]; [self.reactView.contentView.layer removeObserver:self forKeyPath:@"bounds" context:NULL]; } -(void)cleanup { [self removeObservers]; [self killAutoDismissTimer]; } -(void)dealloc { [self cleanup]; } -(void)onRNReload { [self cleanup]; [self removeFromSuperview]; self.reactView = nil; } -(UIColor*)reactViewAvgColor { if (self.reactView.contentView.frame.size.width == 0 || self.reactView.contentView.frame.size.height == 0) { return [UIColor clearColor]; } UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), YES, 0); [self.reactView.contentView drawViewHierarchyInRect:CGRectMake(0, 0, 1, 1) afterScreenUpdates:YES]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); CFDataRef pixelData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage)); const UInt8* data = CFDataGetBytePtr(pixelData); CFRelease(pixelData); //after scale defaults to bgr CGFloat red = data[2] / 255.0f, green = data[1] / 255.0f, blue = data[0] / 255.0f, alpha = data[3] / 255.0f; UIColor *color = [UIColor colorWithRed:red green:green blue:blue alpha:alpha]; return color; } -(BOOL)shouldAddSlidingAnimGapView { if (![[self.params valueForKeyPath:@"animation.animated"] boolValue]) { return NO; } NSString *animationType = [self.params valueForKeyPath:@"animation.type"]; if (![animationType isEqualToString:@"slide-down"]) { return NO; } if (![self.params valueForKeyPath:@"animation.damping"]) { return NO; } CGFloat damping = [[self.params valueForKeyPath:@"animation.damping"] floatValue]; if (damping >= 1) { return NO; } return YES; } -(BOOL)isBottomPosition { return ((self.params[@"position"] != nil) && ([self.params[@"position"] isEqualToString:@"bottom"])); } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { CGSize frameSize = CGSizeZero; if ([object isKindOfClass:[CALayer class]]) frameSize = ((CALayer*)object).frame.size; if ([object isKindOfClass:[UIView class]]) frameSize = ((UIView*)object).frame.size; if (!CGSizeEqualToSize(frameSize, self.reactView.frame.size)) { BOOL isBottomPosition = [self isBottomPosition]; CGFloat yPos = isBottomPosition ? self.superview.bounds.size.height - frameSize.height : 0; self.frame = CGRectMake((self.superview.frame.size.width - frameSize.width) / 2.0, yPos, frameSize.width, frameSize.height); self.reactView.frame = CGRectMake(0, 0, frameSize.width, frameSize.height); self.reactView.contentView.frame = CGRectMake(0, 0, frameSize.width, frameSize.height); //if necessary, add a view with the same color to cover the gap if there's a slide animation with spring if ([self shouldAddSlidingAnimGapView]) { if (self.slideAnimGapView == nil) { self.slideAnimGapView = [[UIView alloc] initWithFrame:CGRectZero]; [self.reactView addSubview:self.slideAnimGapView]; } CGFloat yPos = isBottomPosition ? frameSize.height : -20; self.slideAnimGapView.frame = CGRectMake(0, yPos, self.superview.frame.size.width, 20); dispatch_async(dispatch_get_main_queue(), ^ { self.slideAnimGapView.backgroundColor = [self reactViewAvgColor]; }); } if (self.params[@"shadowRadius"] != nil && [self.params[@"shadowRadius"] floatValue] > 0) { self.layer.shadowPath = [UIBezierPath bezierPathWithRect:self.bounds].CGPath; } } } -(void)performDismiss { [RCCNotification dismissWithParams:self.params resolver:nil rejecter:nil]; } -(void)startAutoDismissTimerIfNecessary { if (self.params[@"autoDismissTimerSec"]) { [self killAutoDismissTimer]; CGFloat interval = [self.params[@"autoDismissTimerSec"] floatValue]; interval = MAX(interval, 1); self.autoDismissTimer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(performDismiss) userInfo:nil repeats:NO]; } } -(void)applyAnimations { NSString *animationType = [self.params valueForKeyPath:@"animation.type"]; if ([animationType isEqualToString:@"swing"]) { CATransform3D transform = CATransform3DIdentity; transform.m34 = -0.002f; transform.m24 = -0.007f; if (self.layer.anchorPoint.x == 0.5 && self.layer.anchorPoint.y == 0.5) { CGPoint anchorPoint = CGPointMake(0.5, 0); CGFloat yOffset = -self.layer.frame.size.height / 2.0; if([self isBottomPosition]) { anchorPoint = CGPointMake(0.5, 1); yOffset = self.layer.frame.size.height / 2.0; } self.layer.anchorPoint = anchorPoint; self.layer.frame = CGRectOffset(self.layer.frame, 0, yOffset); } self.layer.zPosition = 1000; self.layer.transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0); } else { int slideAnimationSign = [self isBottomPosition] ? 1 : -1; CGAffineTransform transform = CGAffineTransformIdentity; if ([animationType isEqualToString:@"slide-down"]) transform = CGAffineTransformMakeTranslation(0, slideAnimationSign * self.frame.size.height); else if ([animationType isEqualToString:@"slide-left"]) transform = CGAffineTransformMakeTranslation(self.frame.size.width, 0); else if ([animationType isEqualToString:@"slide-right"]) transform = CGAffineTransformMakeTranslation(-self.frame.size.width, 0); self.transform = transform; } if ( [[self.params valueForKeyPath:@"animation.fade"] boolValue]) { self.alpha = 0; } } #pragma mark - show/dismiss -(void)showAnimationEnded:(void (^)(void))completion { self.alpha = 1; if (completion) { completion(); } [self startAutoDismissTimerIfNecessary]; } -(void)dismissAnimationEnded:(void (^)(void))completion { if (completion) { completion(); } [self removeFromSuperview]; } -(void)performShowWithCompletion:(void (^)(void))completion { self.hidden = NO; if ([[self.params valueForKeyPath:@"animation.animated"] boolValue]) { CGFloat duration = [[self.params valueForKeyPath:@"animation.duration"] floatValue]; CGFloat delay = [[self.params valueForKeyPath:@"animation.delay"] floatValue]; CGFloat damping = 1; if ([self.params valueForKeyPath:@"animation.damping"] != nil) { damping = [[self.params valueForKeyPath:@"animation.damping"] floatValue]; damping = MAX(damping, 0); damping = MIN(damping, 1); } [self applyAnimations]; [UIView animateWithDuration:duration delay:delay usingSpringWithDamping:damping initialSpringVelocity:0 options:UIViewAnimationOptionCurveEaseOut animations:^() { self.alpha = 1; self.transform = CGAffineTransformIdentity; self.layer.transform = CATransform3DIdentity; } completion:^(BOOL finished) { [self showAnimationEnded:completion]; }]; } else { [self showAnimationEnded:completion]; } } -(void)showWithCompletion:(void (^)(void))completion { if (self.frame.size.height == 0 || self.frame.size.width == 0) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self showWithCompletion:completion]; }); } else { [self performShowWithCompletion:completion]; } } -(void)dismissWithCompletion:(void (^)(void))completion { [self killAutoDismissTimer]; [self.reactView cancelTouches]; if ([[self.params valueForKeyPath:@"animation.animated"] boolValue]) { CGFloat duration = [[self.params valueForKeyPath:@"animation.duration"] floatValue] * 0.75; [UIView animateWithDuration:duration delay:0 usingSpringWithDamping:1 initialSpringVelocity:0 options:UIViewAnimationOptionCurveEaseIn animations:^() { [self applyAnimations]; } completion:^(BOOL finished) { [self dismissAnimationEnded:completion]; }]; } else { [self dismissAnimationEnded:completion]; } } @end @interface PendingNotification : NSObject @property (nonatomic, copy) void (^resolve)(id result); @property (nonatomic, copy) void (^reject)(NSString *code, NSString *message, NSError *error); @property (nonatomic, strong) NSDictionary *params; @end @implementation PendingNotification @end @implementation RCCNotification static NSTimeInterval gLastShownTime = NSTimeIntervalSince1970; static NSMutableArray *gPendingNotifications = nil; static NSMutableArray *gShownNotificationViews = nil; +(void)showWithParams:(NSDictionary*)params resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject { if(gPendingNotifications == nil) gPendingNotifications = [NSMutableArray array]; if(gShownNotificationViews == nil) gShownNotificationViews = [NSMutableArray array]; if ([gShownNotificationViews count] > 0) { for (NotificationView *notificationView in gShownNotificationViews) { [notificationView killAutoDismissTimer]; } //limit the amount of consecutive notifications per second. If they arrive too fast, the last one will be remembered as pending if(CFAbsoluteTimeGetCurrent() - gLastShownTime < 1) { PendingNotification *pendingNotification = [PendingNotification new]; pendingNotification.params = params; pendingNotification.resolve = resolve; pendingNotification.reject = reject; [gPendingNotifications addObject:pendingNotification]; return; } } gLastShownTime = CFAbsoluteTimeGetCurrent(); UIWindow *window = [[RCCManager sharedInstance] getAppWindow]; NotificationView *notificationView = [[NotificationView alloc] initWithParams:params]; [window addSubview:notificationView]; [gShownNotificationViews addObject:notificationView]; [notificationView showWithCompletion:^() { if (resolve) { resolve(nil); } }]; } +(void)dismissWithParams:(NSDictionary*)params resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject { int count = [gShownNotificationViews count]; for (int i = count - 1 ; i >= 0; i--) { NotificationView *notificationView = [gShownNotificationViews objectAtIndex:i]; [gShownNotificationViews removeObject:notificationView]; [notificationView dismissWithCompletion:^() { if (i == (count - 1)) { if(resolve) { resolve(nil); } if ([gPendingNotifications count] > 0) { PendingNotification *pendingNotification = [gPendingNotifications lastObject]; [self showWithParams:pendingNotification.params resolver:pendingNotification.resolve rejecter:pendingNotification.reject]; [gPendingNotifications removeLastObject]; } } }]; } } @end