feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,72 @@
#import <Foundation/Foundation.h>
#import <RNReanimated/LayoutAnimationType.h>
#import <RNReanimated/REANodesManager.h>
#import <RNReanimated/REASnapshot.h>
#import <RNReanimated/REAUIKit.h>
#import <React/RCTUIManager.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, ViewState) {
Inactive,
Appearing,
Disappearing,
Layout,
ToRemove,
};
typedef BOOL (^REAHasAnimationBlock)(NSNumber *_Nonnull tag, LayoutAnimationType type);
typedef BOOL (^REAShouldAnimateExitingBlock)(NSNumber *_Nonnull tag, BOOL shouldAnimate);
typedef void (
^REAAnimationStartingBlock)(NSNumber *_Nonnull tag, LayoutAnimationType type, NSDictionary *_Nonnull yogaValues);
typedef void (^REAAnimationRemovingBlock)(NSNumber *_Nonnull tag);
typedef void (^REASharedTransitionRemovingBlock)(NSNumber *_Nonnull tag);
#ifndef NDEBUG
typedef void (^REACheckDuplicateSharedTagBlock)(REAUIView *view, NSNumber *_Nonnull viewTag);
#endif
typedef void (^REACancelAnimationBlock)(NSNumber *_Nonnull tag);
typedef NSNumber *_Nullable (^REAFindPrecedingViewTagForTransitionBlock)(NSNumber *_Nonnull tag);
typedef int (^REATreeVisitor)(id<RCTComponent>);
BOOL REANodeFind(id<RCTComponent> view, int (^block)(id<RCTComponent>));
@interface REAAnimationsManager : NSObject
- (instancetype)initWithUIManager:(RCTUIManager *)uiManager;
- (void)setAnimationStartingBlock:(REAAnimationStartingBlock)startAnimation;
- (void)setHasAnimationBlock:(REAHasAnimationBlock)hasAnimation;
- (void)setShouldAnimateExitingBlock:(REAShouldAnimateExitingBlock)shouldAnimateExiting;
- (void)setAnimationRemovingBlock:(REAAnimationRemovingBlock)clearAnimation;
- (void)setSharedTransitionRemovingBlock:(REASharedTransitionRemovingBlock)clearSharedTransition;
#ifndef NDEBUG
- (void)setCheckDuplicateSharedTagBlock:(REACheckDuplicateSharedTagBlock)checkDuplicateSharedTag;
#endif
- (void)progressLayoutAnimationWithStyle:(NSDictionary *_Nonnull)newStyle
forTag:(NSNumber *_Nonnull)tag
isSharedTransition:(BOOL)isSharedTransition;
- (void)setFindPrecedingViewTagForTransitionBlock:
(REAFindPrecedingViewTagForTransitionBlock)findPrecedingViewTagForTransition;
- (void)setCancelAnimationBlock:(REACancelAnimationBlock)animationCancellingBlock;
- (void)endLayoutAnimationForTag:(NSNumber *_Nonnull)tag removeView:(BOOL)removeView;
- (void)endAnimationsRecursive:(REAUIView *)view;
- (void)invalidate;
- (void)viewDidMount:(REAUIView *)view withBeforeSnapshot:(REASnapshot *)snapshot withNewFrame:(CGRect)frame;
- (REASnapshot *)prepareSnapshotBeforeMountForView:(REAUIView *)view;
- (void)removeAnimationsFromSubtree:(REAUIView *)view;
- (void)reattachAnimatedChildren:(NSArray<id<RCTComponent>> *)children
toContainer:(id<RCTComponent>)container
atIndices:(NSArray<NSNumber *> *)indices;
- (void)onViewCreate:(REAUIView *)view after:(REASnapshot *)after;
- (void)onViewUpdate:(REAUIView *)view before:(REASnapshot *)before after:(REASnapshot *)after;
- (void)viewsDidLayout;
- (NSMutableDictionary *)prepareDataForLayoutAnimatingWorklet:(NSMutableDictionary *)currentValues
targetValues:(NSMutableDictionary *)targetValues;
- (REAUIView *)viewForTag:(NSNumber *)tag;
- (BOOL)hasAnimationForTag:(NSNumber *)tag type:(LayoutAnimationType)type;
- (void)clearAnimationConfigForTag:(NSNumber *)tag;
- (void)clearSharedTransitionConfigForTag:(NSNumber *)tag;
- (void)startAnimationForTag:(NSNumber *)tag type:(LayoutAnimationType)type yogaValues:(NSDictionary *)yogaValues;
- (void)onScreenRemoval:(REAUIView *)screen stack:(REAUIView *)stack;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,638 @@
#import <RNReanimated/REAAnimationsManager.h>
#import <RNReanimated/REASharedElement.h>
#import <RNReanimated/REASharedTransitionManager.h>
#import <RNReanimated/REASwizzledUIManager.h>
#import <React/RCTComponentData.h>
#import <React/RCTTextView.h>
#import <React/UIView+Private.h>
#import <React/UIView+React.h>
typedef NS_ENUM(NSInteger, FrameConfigType) { EnteringFrame, ExitingFrame };
BOOL REANodeFind(id<RCTComponent> view, int (^block)(id<RCTComponent>))
{
if (!view.reactTag) {
return NO;
}
if (block(view)) {
return YES;
}
for (id<RCTComponent> subview in view.reactSubviews) {
if (REANodeFind(subview, block)) {
return YES;
}
}
return NO;
}
@implementation REAAnimationsManager {
RCTUIManager *_uiManager;
REASwizzledUIManager *_reaSwizzledUIManager;
NSMutableSet<NSNumber *> *_enteringViews;
NSMutableDictionary<NSNumber *, REASnapshot *> *_enteringViewTargetValues;
NSMutableDictionary<NSNumber *, REAUIView *> *_exitingViews;
NSMutableDictionary<NSNumber *, NSNumber *> *_exitingSubviewsCountMap;
NSMutableDictionary<NSNumber *, NSNumber *> *_exitingParentTags;
NSMutableSet<NSNumber *> *_ancestorsToRemove;
NSMutableArray<NSString *> *_targetKeys;
NSMutableArray<NSString *> *_currentKeys;
REAAnimationStartingBlock _startAnimationForTag;
REAHasAnimationBlock _hasAnimationForTag;
REAShouldAnimateExitingBlock _shouldAnimateExiting;
REAAnimationRemovingBlock _clearAnimationConfigForTag;
REASharedTransitionRemovingBlock _clearSharedTransitionConfigForTag;
REASharedTransitionManager *_sharedTransitionManager;
#ifndef NDEBUG
REACheckDuplicateSharedTagBlock _checkDuplicateSharedTag;
#endif
}
+ (NSArray *)layoutKeys
{
static NSArray *_array;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_array = @[ @"originX", @"originY", @"width", @"height" ];
});
return _array;
}
- (instancetype)initWithUIManager:(RCTUIManager *)uiManager
{
if (self = [super init]) {
_uiManager = uiManager;
_exitingViews = [NSMutableDictionary new];
_exitingSubviewsCountMap = [NSMutableDictionary new];
_ancestorsToRemove = [NSMutableSet new];
_exitingParentTags = [NSMutableDictionary new];
_enteringViews = [NSMutableSet new];
_enteringViewTargetValues = [NSMutableDictionary new];
_targetKeys = [NSMutableArray new];
_currentKeys = [NSMutableArray new];
for (NSString *key in [[self class] layoutKeys]) {
[_targetKeys addObject:[NSString stringWithFormat:@"target%@", [key capitalizedString]]];
[_currentKeys addObject:[NSString stringWithFormat:@"current%@", [key capitalizedString]]];
}
_sharedTransitionManager = [[REASharedTransitionManager alloc] initWithAnimationsManager:self];
_reaSwizzledUIManager = [[REASwizzledUIManager alloc] initWithUIManager:uiManager withAnimationManager:self];
_startAnimationForTag = ^(NSNumber *tag, LayoutAnimationType type, NSDictionary *yogaValues) {
// default implementation, this block will be replaced by a setter
};
_hasAnimationForTag = ^(NSNumber *tag, LayoutAnimationType type) {
// default implementation, this block will be replaced by a setter
return NO;
};
_shouldAnimateExiting = ^(NSNumber *tag, BOOL shouldAnimate) {
// default implementation, this block will be replaced by a setter
return YES;
};
_clearAnimationConfigForTag = ^(NSNumber *tag) {
// default implementation, this block will be replaced by a setter
};
_clearSharedTransitionConfigForTag = ^(NSNumber *tag) {
// default implementation, this block will be replaced by a setter
};
#ifndef NDEBUG
_checkDuplicateSharedTag = ^(REAUIView *view, NSNumber *viewTag) {
// default implementation, this block will be replaced by a setter
};
#endif
}
return self;
}
- (void)invalidate
{
_startAnimationForTag = nil;
_hasAnimationForTag = nil;
_uiManager = nil;
_exitingViews = nil;
_targetKeys = nil;
_currentKeys = nil;
}
- (void)setAnimationStartingBlock:(REAAnimationStartingBlock)startAnimation
{
_startAnimationForTag = startAnimation;
}
- (void)setHasAnimationBlock:(REAHasAnimationBlock)hasAnimation
{
_hasAnimationForTag = hasAnimation;
}
- (void)setShouldAnimateExitingBlock:(REAShouldAnimateExitingBlock)shouldAnimateExiting
{
_shouldAnimateExiting = shouldAnimateExiting;
}
- (void)setAnimationRemovingBlock:(REAAnimationRemovingBlock)clearAnimation
{
_clearAnimationConfigForTag = clearAnimation;
}
- (void)setSharedTransitionRemovingBlock:(REASharedTransitionRemovingBlock)clearSharedTransition
{
_clearSharedTransitionConfigForTag = clearSharedTransition;
}
#ifndef NDEBUG
- (void)setCheckDuplicateSharedTagBlock:(REACheckDuplicateSharedTagBlock)checkDuplicateSharedTag
{
_checkDuplicateSharedTag = checkDuplicateSharedTag;
}
#endif
- (REAUIView *)viewForTag:(NSNumber *)tag
{
REAUIView *view;
(view = [_uiManager viewForReactTag:tag]) || (view = [_exitingViews objectForKey:tag]) ||
(view = [_sharedTransitionManager getTransitioningView:tag]);
return view;
}
- (void)endLayoutAnimationForTag:(NSNumber *)tag removeView:(BOOL)removeView
{
REAUIView *view = [self viewForTag:tag];
if (view == nil) {
return;
}
if ([_enteringViews containsObject:tag] && !removeView) {
REASnapshot *target = _enteringViewTargetValues[tag];
if (target != nil) {
[self setNewProps:target.values forView:view];
}
}
[_enteringViews removeObject:tag];
[_enteringViewTargetValues removeObjectForKey:tag];
if (removeView) {
[self endAnimationsRecursive:view];
[view removeFromSuperview];
}
[_sharedTransitionManager finishSharedAnimation:[self viewForTag:tag] removeView:removeView];
}
- (void)endAnimationsRecursive:(REAUIView *)view
{
NSNumber *tag = [view reactTag];
if (tag == nil) {
return;
}
// we'll remove this view anyway when exiting from recursion,
// no need to remove it in `maybeDropAncestors`
[_ancestorsToRemove removeObject:tag];
for (REAUIView *child in [[view subviews] copy]) {
[self endAnimationsRecursive:child];
}
if ([_exitingViews objectForKey:tag]) {
[_exitingViews removeObjectForKey:tag];
[self maybeDropAncestors:view];
}
}
- (void)progressLayoutAnimationWithStyle:(NSDictionary *)newStyle
forTag:(NSNumber *)tag
isSharedTransition:(BOOL)isSharedTransition
{
[self setNewProps:[newStyle mutableCopy] forView:[self viewForTag:tag] convertFromAbsolute:isSharedTransition];
}
- (double)getDoubleOrZero:(NSNumber *)number
{
double doubleValue = [number doubleValue];
if (doubleValue != doubleValue) { // NaN != NaN
return 0;
}
return doubleValue;
}
- (void)setNewProps:(NSMutableDictionary *)newProps forView:(REAUIView *)view
{
[self setNewProps:newProps forView:view convertFromAbsolute:NO];
}
- (void)setNewProps:(NSMutableDictionary *)newProps
forView:(REAUIView *)view
convertFromAbsolute:(BOOL)convertFromAbsolute
{
if (newProps[@"height"]) {
double height = [self getDoubleOrZero:newProps[@"height"]];
double oldHeight = view.bounds.size.height;
view.bounds = CGRectMake(0, 0, view.bounds.size.width, height);
view.center = CGPointMake(view.center.x, view.center.y - oldHeight / 2.0 + view.bounds.size.height / 2.0);
[newProps removeObjectForKey:@"height"];
}
if (newProps[@"width"]) {
double width = [self getDoubleOrZero:newProps[@"width"]];
double oldWidth = view.bounds.size.width;
view.bounds = CGRectMake(0, 0, width, view.bounds.size.height);
view.center = CGPointMake(view.center.x + view.bounds.size.width / 2.0 - oldWidth / 2.0, view.center.y);
[newProps removeObjectForKey:@"width"];
}
bool needsViewPositionUpdate = false;
double centerX = view.center.x;
double centerY = view.center.y;
if (newProps[@"originX"]) {
needsViewPositionUpdate = true;
double originX = [self getDoubleOrZero:newProps[@"originX"]];
[newProps removeObjectForKey:@"originX"];
centerX = originX + view.bounds.size.width / 2.0;
}
if (newProps[@"originY"]) {
needsViewPositionUpdate = true;
double originY = [self getDoubleOrZero:newProps[@"originY"]];
[newProps removeObjectForKey:@"originY"];
centerY = originY + view.bounds.size.height / 2.0;
}
if (needsViewPositionUpdate) {
CGPoint newCenter = CGPointMake(centerX, centerY);
if (convertFromAbsolute) {
#if TARGET_OS_OSX
REAUIView *window = UIApplication.sharedApplication.keyWindow;
#else
REAUIView *window = RCTKeyWindow();
#endif
CGPoint convertedCenter = [window convertPoint:newCenter toView:view.superview];
view.center = convertedCenter;
} else {
view.center = newCenter;
}
}
if (newProps[@"transformMatrix"]) {
NSArray *matrix = newProps[@"transformMatrix"];
CGFloat a = [matrix[0] floatValue];
CGFloat b = [matrix[1] floatValue];
CGFloat c = [matrix[3] floatValue];
CGFloat d = [matrix[4] floatValue];
CGFloat tx = [matrix[6] floatValue];
CGFloat ty = [matrix[7] floatValue];
view.transform = CGAffineTransformMake(a, b, c, d, tx, ty);
[newProps removeObjectForKey:@"transformMatrix"];
}
NSMutableDictionary *componentDataByName = [_uiManager valueForKey:@"_componentDataByName"];
RCTComponentData *componentData = componentDataByName[@"RCTView"];
[componentData setProps:newProps forView:view];
}
- (NSDictionary *)prepareDataForAnimatingWorklet:(NSMutableDictionary *)values frameConfig:(FrameConfigType)frameConfig
{
if (frameConfig == EnteringFrame) {
NSDictionary *preparedData = @{
@"targetWidth" : values[@"width"],
@"targetHeight" : values[@"height"],
@"targetOriginX" : values[@"originX"],
@"targetOriginY" : values[@"originY"],
@"targetGlobalOriginX" : values[@"globalOriginX"],
@"targetGlobalOriginY" : values[@"globalOriginY"],
@"windowWidth" : values[@"windowWidth"],
@"windowHeight" : values[@"windowHeight"]
};
return preparedData;
} else {
NSDictionary *preparedData = @{
@"currentWidth" : values[@"width"],
@"currentHeight" : values[@"height"],
@"currentOriginX" : values[@"originX"],
@"currentOriginY" : values[@"originY"],
@"currentGlobalOriginX" : values[@"globalOriginX"],
@"currentGlobalOriginY" : values[@"globalOriginY"],
@"windowWidth" : values[@"windowWidth"],
@"windowHeight" : values[@"windowHeight"]
};
return preparedData;
}
}
- (NSMutableDictionary *)prepareDataForLayoutAnimatingWorklet:(NSMutableDictionary *)currentValues
targetValues:(NSMutableDictionary *)targetValues
{
NSMutableDictionary *preparedData = [NSMutableDictionary new];
preparedData[@"currentWidth"] = currentValues[@"width"];
preparedData[@"currentHeight"] = currentValues[@"height"];
preparedData[@"currentOriginX"] = currentValues[@"originX"];
preparedData[@"currentOriginY"] = currentValues[@"originY"];
preparedData[@"currentGlobalOriginX"] = currentValues[@"globalOriginX"];
preparedData[@"currentGlobalOriginY"] = currentValues[@"globalOriginY"];
preparedData[@"targetWidth"] = targetValues[@"width"];
preparedData[@"targetHeight"] = targetValues[@"height"];
preparedData[@"targetOriginX"] = targetValues[@"originX"];
preparedData[@"targetOriginY"] = targetValues[@"originY"];
preparedData[@"targetGlobalOriginX"] = targetValues[@"globalOriginX"];
preparedData[@"targetGlobalOriginY"] = targetValues[@"globalOriginY"];
preparedData[@"windowWidth"] = currentValues[@"windowWidth"];
preparedData[@"windowHeight"] = currentValues[@"windowHeight"];
return preparedData;
}
- (void)registerExitingAncestors:(REAUIView *)child
{
[self registerExitingAncestors:child exitingSubviewsCount:1];
}
- (void)registerExitingAncestors:(REAUIView *)child exitingSubviewsCount:(int)exitingSubviewsCount
{
NSNumber *childTag = child.reactTag;
REAUIView *parent = child.superview;
UIViewController *childController = child.reactViewController;
// only register ancestors whose `reactViewController` is the same as `child`'s.
// The idea is that, if a whole ViewController is unmounted, we won't want to run
// the exiting animation since all the views will disappear immediately anyway
while (parent != nil && parent.reactViewController == childController &&
![parent isKindOfClass:[RCTRootView class]]) {
NSNumber *parentTag = parent.reactTag;
if (parentTag != nil) {
_exitingSubviewsCountMap[parent.reactTag] =
@([_exitingSubviewsCountMap[parent.reactTag] intValue] + exitingSubviewsCount);
_exitingParentTags[childTag] = parentTag;
childTag = parentTag;
}
parent = parent.superview;
}
}
- (void)maybeDropAncestors:(REAUIView *)child
{
REAUIView *parent = child.superview;
NSNumber *parentTag = _exitingParentTags[child.reactTag];
[_exitingParentTags removeObjectForKey:child.reactTag];
while ((parent != nil || parentTag != nil) && ![parent isKindOfClass:[RCTRootView class]]) {
REAUIView *view = parent;
NSNumber *viewTag = parentTag;
parentTag = _exitingParentTags[viewTag];
REAUIView *viewByTag = [self viewForTag:viewTag];
parent = view.superview;
if (view == nil) {
if (viewByTag == nil) {
// the view was already removed from both native and RN hierarchies
// we can safely forget that it had any animated children
[_ancestorsToRemove removeObject:viewTag];
[_exitingSubviewsCountMap removeObjectForKey:viewTag];
[_exitingParentTags removeObjectForKey:viewTag];
continue;
}
// the child was dettached from view, but view is still
// in the native and RN hierarchy
view = viewByTag;
}
if (view.reactTag == nil) {
// we skip over views with no tag when registering parent tags,
// so we shouldn't go to the parent of viewTag yet
parentTag = viewTag;
continue;
}
int trackingCount = [_exitingSubviewsCountMap[view.reactTag] intValue] - 1;
if (trackingCount <= 0) {
if ([_ancestorsToRemove containsObject:view.reactTag]) {
[_ancestorsToRemove removeObject:view.reactTag];
if (![_exitingViews objectForKey:view.reactTag]) {
[view removeFromSuperview];
}
}
[_exitingSubviewsCountMap removeObjectForKey:view.reactTag];
[_exitingParentTags removeObjectForKey:view.reactTag];
} else {
_exitingSubviewsCountMap[view.reactTag] = @(trackingCount);
}
}
}
- (BOOL)startAnimationsRecursive:(REAUIView *)view
shouldRemoveSubviewsWithoutAnimations:(BOOL)shouldRemoveSubviewsWithoutAnimations
shouldAnimate:(BOOL)shouldAnimate;
{
if (!view.reactTag) {
return NO;
}
UIViewController *viewController = view.reactViewController;
// `startAnimationsRecursive:shouldRemoveSubviewsWithoutAnimations:`
// is called on a detached view tree, so the `viewController` should be `nil`.
// If it's not, we're descending into another `UIViewController`.
// We don't want to run animations inside it (since it causes issues with RNScreens),
// so instead clean up the subtree and return `NO`.
if (viewController != nil) {
[self removeAnimationsFromSubtree:view];
return NO;
}
shouldAnimate = [self shouldAnimateExiting:view.reactTag shouldAnimate:shouldAnimate];
BOOL hasExitAnimation = shouldAnimate &&
([self hasAnimationForTag:view.reactTag type:EXITING] || [_exitingViews objectForKey:view.reactTag]);
BOOL hasAnimatedChildren = NO;
shouldRemoveSubviewsWithoutAnimations = shouldRemoveSubviewsWithoutAnimations && !hasExitAnimation;
NSMutableArray *toBeRemoved = [[NSMutableArray alloc] init];
for (REAUIView *subview in [view.reactSubviews copy]) {
if ([self startAnimationsRecursive:subview
shouldRemoveSubviewsWithoutAnimations:shouldRemoveSubviewsWithoutAnimations
shouldAnimate:shouldAnimate]) {
hasAnimatedChildren = YES;
} else if (shouldRemoveSubviewsWithoutAnimations) {
[toBeRemoved addObject:subview];
}
}
BOOL wantAnimateExit = hasExitAnimation || hasAnimatedChildren;
REASnapshot *before;
if (hasExitAnimation) {
before = [[REASnapshot alloc] init:view];
}
// start exit animation
if (hasExitAnimation && ![_exitingViews objectForKey:view.reactTag]) {
NSDictionary *preparedValues = [self prepareDataForAnimatingWorklet:before.values frameConfig:ExitingFrame];
[_exitingViews setObject:view forKey:view.reactTag];
[self registerExitingAncestors:view];
_startAnimationForTag(view.reactTag, EXITING, preparedValues);
}
// NOTE: even though this view is still visible,
// since it's removed from the React tree, we won't
// start new animations for it, and might as well remove
// the layout animation config now
_clearAnimationConfigForTag(view.reactTag);
if (!wantAnimateExit) {
return NO;
}
if (hasAnimatedChildren) {
[_ancestorsToRemove addObject:view.reactTag];
}
for (REAUIView *child in toBeRemoved) {
[view removeReactSubview:child];
}
// we don't want user interaction on exiting views
view.userInteractionEnabled = NO;
return YES;
}
- (void)reattachAnimatedChildren:(NSArray<id<RCTComponent>> *)children
toContainer:(id<RCTComponent>)container
atIndices:(NSArray<NSNumber *> *)indices
{
if (![container isKindOfClass:[REAUIView class]]) {
return;
}
// since we reattach only some of the views,
// we count the views we DIDN'T reattach
// and shift later views' indices by that number
// to make sure they appear at correct relative posisitons
// in the `subviews` array
int skippedViewsCount = 0;
for (int i = 0; i < children.count; i++) {
id<RCTComponent> child = children[i];
if (![child isKindOfClass:[REAUIView class]]) {
skippedViewsCount++;
continue;
}
REAUIView *childView = (REAUIView *)child;
NSNumber *originalIndex = indices[i];
if ([self startAnimationsRecursive:childView shouldRemoveSubviewsWithoutAnimations:YES shouldAnimate:YES]) {
[(REAUIView *)container insertSubview:childView atIndex:[originalIndex intValue] - skippedViewsCount];
int exitingSubviewsCount = [_exitingSubviewsCountMap[childView.reactTag] intValue];
if ([_exitingViews objectForKey:childView.reactTag] != nil) {
exitingSubviewsCount++;
}
[self registerExitingAncestors:childView exitingSubviewsCount:exitingSubviewsCount];
} else {
skippedViewsCount++;
}
}
}
- (void)onViewCreate:(REAUIView *)view after:(REASnapshot *)after
{
NSMutableDictionary *targetValues = after.values;
NSDictionary *preparedValues = [self prepareDataForAnimatingWorklet:targetValues frameConfig:EnteringFrame];
[_enteringViews addObject:view.reactTag];
_startAnimationForTag(view.reactTag, ENTERING, preparedValues);
}
- (void)onViewUpdate:(REAUIView *)view before:(REASnapshot *)before after:(REASnapshot *)after
{
NSMutableDictionary *targetValues = after.values;
NSMutableDictionary *currentValues = before.values;
NSDictionary *preparedValues = [self prepareDataForLayoutAnimatingWorklet:currentValues targetValues:targetValues];
_startAnimationForTag(view.reactTag, LAYOUT, preparedValues);
}
- (REASnapshot *)prepareSnapshotBeforeMountForView:(REAUIView *)view
{
return [[REASnapshot alloc] init:view];
}
- (void)removeAnimationsFromSubtree:(REAUIView *)view
{
REANodeFind(view, ^int(id<RCTComponent> view) {
if (!self->_hasAnimationForTag(view.reactTag, SHARED_ELEMENT_TRANSITION)) {
self->_clearAnimationConfigForTag(view.reactTag);
}
return false;
});
}
- (void)viewDidMount:(REAUIView *)view withBeforeSnapshot:(nonnull REASnapshot *)before withNewFrame:(CGRect)frame
{
LayoutAnimationType type = before == nil ? ENTERING : LAYOUT;
NSNumber *viewTag = view.reactTag;
if (_hasAnimationForTag(viewTag, type)) {
REASnapshot *after = [[REASnapshot alloc] init:view];
if (before == nil) {
[self onViewCreate:view after:after];
} else {
[self onViewUpdate:view before:before after:after];
}
} else if (type == LAYOUT && [_enteringViews containsObject:[view reactTag]]) {
_enteringViewTargetValues[[view reactTag]] = [[REASnapshot alloc] init:view];
[self setNewProps:before.values forView:view];
}
if (_hasAnimationForTag(viewTag, SHARED_ELEMENT_TRANSITION)) {
if (type == ENTERING) {
[_sharedTransitionManager notifyAboutNewView:view];
#ifndef NDEBUG
_checkDuplicateSharedTag(view, viewTag);
#endif
} else {
[_sharedTransitionManager notifyAboutViewLayout:view withViewFrame:frame];
}
}
}
- (void)viewsDidLayout
{
[_sharedTransitionManager viewsDidLayout];
}
- (void)setFindPrecedingViewTagForTransitionBlock:
(REAFindPrecedingViewTagForTransitionBlock)findPrecedingViewTagForTransition
{
[_sharedTransitionManager setFindPrecedingViewTagForTransitionBlock:findPrecedingViewTagForTransition];
}
- (void)setCancelAnimationBlock:(REACancelAnimationBlock)animationCancellingBlock
{
[_sharedTransitionManager setCancelAnimationBlock:animationCancellingBlock];
}
- (BOOL)hasAnimationForTag:(NSNumber *)tag type:(LayoutAnimationType)type
{
return _hasAnimationForTag(tag, type);
}
- (BOOL)shouldAnimateExiting:(NSNumber *)tag shouldAnimate:(BOOL)shouldAnimate
{
return _shouldAnimateExiting(tag, shouldAnimate);
}
- (void)clearAnimationConfigForTag:(NSNumber *)tag
{
_clearAnimationConfigForTag(tag);
}
- (void)clearSharedTransitionConfigForTag:(NSNumber *)tag
{
_clearSharedTransitionConfigForTag(tag);
}
- (void)startAnimationForTag:(NSNumber *)tag type:(LayoutAnimationType)type yogaValues:(NSDictionary *)yogaValues
{
_startAnimationForTag(tag, type, yogaValues);
}
- (void)onScreenRemoval:(REAUIView *)screen stack:(REAUIView *)stack
{
[_sharedTransitionManager onScreenRemoval:screen stack:stack];
}
@end

View File

@@ -0,0 +1,10 @@
@interface REAFrame : NSObject
@property float x;
@property float y;
@property float width;
@property float height;
- (instancetype)initWithX:(float)x y:(float)y width:(float)width height:(float)height;
@end

View File

@@ -0,0 +1,15 @@
#import <RNReanimated/REAFrame.h>
@implementation REAFrame
- (instancetype)initWithX:(float)x y:(float)y width:(float)width height:(float)height
{
self = [super init];
_x = x;
_y = y;
_width = width;
_height = height;
return self;
}
@end

View File

@@ -0,0 +1,21 @@
#define LOAD_SCREENS_HEADERS \
((!RCT_NEW_ARCH_ENABLED && __has_include(<RNScreens/RNSScreen.h>)) \
|| (RCT_NEW_ARCH_ENABLED && __has_include(<RNScreens/RNSScreen.h>) && __cplusplus))
#if LOAD_SCREENS_HEADERS
#import <RNScreens/RNSScreen.h>
#import <RNScreens/RNSScreenStack.h>
#endif
#import <RNReanimated/REAUIKit.h>
@interface REAScreensHelper : NSObject
+ (REAUIView *)getScreenForView:(REAUIView *)view;
+ (REAUIView *)getStackForView:(REAUIView *)view;
+ (bool)isScreenModal:(REAUIView *)screen;
+ (REAUIView *)getScreenWrapper:(REAUIView *)view;
+ (int)getScreenType:(REAUIView *)screen;
+ (bool)isRNSScreenType:(REAUIView *)screen;
@end

View File

@@ -0,0 +1,106 @@
#import <RNReanimated/REAScreensHelper.h>
@implementation REAScreensHelper
#if LOAD_SCREENS_HEADERS
+ (REAUIView *)getScreenForView:(REAUIView *)view
{
REAUIView *screen = view;
while (![screen isKindOfClass:[RNSScreenView class]] && screen.superview != nil) {
screen = screen.superview;
}
if ([screen isKindOfClass:[RNSScreenView class]]) {
return screen;
}
return nil;
}
+ (REAUIView *)getStackForView:(REAUIView *)view
{
if ([view isKindOfClass:[RNSScreenView class]]) {
if (view.reactSuperview != nil) {
if ([view.reactSuperview isKindOfClass:[RNSScreenStackView class]]) {
return view.reactSuperview;
}
}
}
while (view != nil && ![view isKindOfClass:[RNSScreenStackView class]] && view.superview != nil) {
view = view.superview;
}
if ([view isKindOfClass:[RNSScreenStackView class]]) {
return view;
}
return nil;
}
+ (bool)isScreenModal:(REAUIView *)uiViewScreen
{
if ([uiViewScreen isKindOfClass:[RNSScreenView class]]) {
RNSScreenView *screen = (RNSScreenView *)uiViewScreen;
bool isModal = [screen isModal];
if (!isModal) {
// case for modal with header
RNSScreenView *parentScreen = (RNSScreenView *)[REAScreensHelper getScreenForView:screen.reactSuperview];
if (parentScreen != nil) {
isModal = [parentScreen isModal];
}
}
return isModal;
}
return false;
}
+ (REAUIView *)getScreenWrapper:(REAUIView *)view
{
REAUIView *screen = [REAScreensHelper getScreenForView:view];
REAUIView *stack = [REAScreensHelper getStackForView:screen];
REAUIView *screenWrapper = [REAScreensHelper getScreenForView:stack];
return screenWrapper;
}
+ (int)getScreenType:(REAUIView *)screen;
{
return [[screen valueForKey:@"stackPresentation"] intValue];
}
+ (bool)isRNSScreenType:(REAUIView *)view
{
return [view isKindOfClass:[RNSScreen class]] == YES;
}
#else
+ (REAUIView *)getScreenForView:(REAUIView *)view
{
return nil;
}
+ (REAUIView *)getStackForView:(REAUIView *)view
{
return nil;
}
+ (bool)isScreenModal:(REAUIView *)screen
{
return false;
}
+ (REAUIView *)getScreenWrapper:(REAUIView *)view
{
return nil;
}
+ (int)getScreenType:(REAUIView *)screen;
{
return 0;
}
+ (bool)isRNSScreenType:(REAUIView *)screen
{
return false;
}
#endif // LOAD_SCREENS_HEADERS
@end

View File

@@ -0,0 +1,18 @@
#import <RNReanimated/LayoutAnimationType.h>
#import <RNReanimated/REASnapshot.h>
#import <RNReanimated/REAUIKit.h>
@interface REASharedElement : NSObject
- (instancetype)initWithSourceView:(REAUIView *)sourceView
sourceViewSnapshot:(REASnapshot *)sourceViewSnapshot
targetView:(REAUIView *)targetView
targetViewSnapshot:(REASnapshot *)targetViewSnapshot;
@property REAUIView *sourceView;
@property REASnapshot *sourceViewSnapshot;
@property REAUIView *targetView;
@property REASnapshot *targetViewSnapshot;
@property LayoutAnimationType animationType;
@end

View File

@@ -0,0 +1,17 @@
#import <RNReanimated/REASharedElement.h>
@implementation REASharedElement
- (instancetype)initWithSourceView:(REAUIView *)sourceView
sourceViewSnapshot:(REASnapshot *)sourceViewSnapshot
targetView:(REAUIView *)targetView
targetViewSnapshot:(REASnapshot *)targetViewSnapshot
{
self = [super init];
_sourceView = sourceView;
_sourceViewSnapshot = sourceViewSnapshot;
_targetView = targetView;
_targetViewSnapshot = targetViewSnapshot;
_animationType = SHARED_ELEMENT_TRANSITION;
return self;
}
@end

View File

@@ -0,0 +1,19 @@
#import <RNReanimated/REAAnimationsManager.h>
#import <RNReanimated/REASnapshot.h>
@interface REASharedTransitionManager : NSObject
- (void)notifyAboutNewView:(REAUIView *)view;
- (void)notifyAboutViewLayout:(REAUIView *)view withViewFrame:(CGRect)frame;
- (void)viewsDidLayout;
- (void)finishSharedAnimation:(REAUIView *)view removeView:(BOOL)removeView;
- (void)setFindPrecedingViewTagForTransitionBlock:
(REAFindPrecedingViewTagForTransitionBlock)findPrecedingViewTagForTransition;
- (void)setCancelAnimationBlock:(REACancelAnimationBlock)cancelAnimationBlock;
- (instancetype)initWithAnimationsManager:(REAAnimationsManager *)animationManager;
- (REAUIView *)getTransitioningView:(NSNumber *)tag;
- (NSDictionary *)prepareDataForWorklet:(NSMutableDictionary *)currentValues
targetValues:(NSMutableDictionary *)targetValues;
- (void)onScreenRemoval:(REAUIView *)screen stack:(REAUIView *)stack;
@end

View File

@@ -0,0 +1,823 @@
#import <RNReanimated/REAFrame.h>
#import <RNReanimated/REAScreensHelper.h>
#import <RNReanimated/REASharedElement.h>
#import <RNReanimated/REASharedTransitionManager.h>
#import <RNReanimated/REAUtils.h>
@implementation REASharedTransitionManager {
NSMutableDictionary<NSNumber *, REAUIView *> *_sharedTransitionParent;
NSMutableDictionary<NSNumber *, NSNumber *> *_sharedTransitionInParentIndex;
NSMutableDictionary<NSNumber *, REASnapshot *> *_snapshotRegistry;
NSMutableDictionary<NSNumber *, REAUIView *> *_currentSharedTransitionViews;
REAFindPrecedingViewTagForTransitionBlock _findPrecedingViewTagForTransition;
REACancelAnimationBlock _cancelLayoutAnimation;
REAUIView *_transitionContainer;
NSMutableArray<REAUIView *> *_addedSharedViews;
BOOL _isSharedTransitionActive;
NSMutableArray<REASharedElement *> *_sharedElements;
NSMutableDictionary<NSNumber *, REASharedElement *> *_sharedElementsLookup;
REAAnimationsManager *_animationManager;
NSMutableSet<NSNumber *> *_viewsToHide;
NSMutableArray<REAUIView *> *_removedViews;
NSMutableSet<REAUIView *> *_viewsWithCanceledAnimation;
NSMutableDictionary<NSNumber *, NSNumber *> *_disableCleaningForView;
NSMutableDictionary<NSNumber *, REAUIView *> *_removedViewRegistry;
NSMutableSet<NSNumber *> *_layoutedSharedViewsTags;
NSMutableDictionary<NSNumber *, REAFrame *> *_layoutedSharedViewsFrame;
NSMutableSet<REAUIView *> *_reattachedViews;
BOOL _isStackDropped;
BOOL _isAsyncSharedTransitionConfigured;
BOOL _isConfigured;
BOOL _clearScreen;
BOOL _isInteractive;
REAUIView *_disappearingScreen;
}
/*
`_sharedTransitionManager` provides access to current REASharedTransitionManager
instance from swizzled methods in react-native-screens. Swizzled method has
different context of execution (self != REASharedTransitionManager)
*/
static REASharedTransitionManager *_sharedTransitionManager;
- (instancetype)initWithAnimationsManager:(REAAnimationsManager *)animationManager
{
if (self = [super init]) {
_snapshotRegistry = [NSMutableDictionary new];
_currentSharedTransitionViews = [NSMutableDictionary new];
_addedSharedViews = [NSMutableArray new];
_sharedTransitionParent = [NSMutableDictionary new];
_sharedTransitionInParentIndex = [NSMutableDictionary new];
_isSharedTransitionActive = NO;
_sharedElements = [NSMutableArray new];
_sharedElementsLookup = [NSMutableDictionary new];
_animationManager = animationManager;
_viewsToHide = [NSMutableSet new];
_sharedTransitionManager = self;
_disableCleaningForView = [NSMutableDictionary new];
_removedViewRegistry = [NSMutableDictionary new];
_layoutedSharedViewsTags = [NSMutableSet new];
_layoutedSharedViewsFrame = [NSMutableDictionary new];
_reattachedViews = [NSMutableSet new];
_isAsyncSharedTransitionConfigured = NO;
_isConfigured = NO;
[self swizzleScreensMethods];
}
return self;
}
- (void)invalidate
{
_snapshotRegistry = nil;
_currentSharedTransitionViews = nil;
_addedSharedViews = nil;
_sharedTransitionParent = nil;
_sharedTransitionInParentIndex = nil;
_sharedElements = nil;
_animationManager = nil;
}
- (REAUIView *)getTransitioningView:(NSNumber *)tag
{
REAUIView *view = _currentSharedTransitionViews[tag];
if (view == nil) {
return _removedViewRegistry[tag];
}
return view;
}
- (void)notifyAboutNewView:(REAUIView *)view
{
if (!_isConfigured) {
return;
}
[_addedSharedViews addObject:view];
}
- (void)notifyAboutViewLayout:(REAUIView *)view withViewFrame:(CGRect)frame
{
if (!_isConfigured) {
return;
}
[_layoutedSharedViewsTags addObject:view.reactTag];
float x = frame.origin.x;
float y = frame.origin.y;
float width = frame.size.width;
float height = frame.size.height;
_layoutedSharedViewsFrame[view.reactTag] = [[REAFrame alloc] initWithX:x y:y width:width height:height];
}
- (void)viewsDidLayout
{
if (!_isConfigured) {
return;
}
[self configureAsyncSharedTransitionForViews:_addedSharedViews];
[_addedSharedViews removeAllObjects];
[self maybeRestartAnimationWithNewLayout];
[_layoutedSharedViewsTags removeAllObjects];
[_layoutedSharedViewsFrame removeAllObjects];
}
- (void)configureAsyncSharedTransitionForViews:(NSArray<REAUIView *> *)views
{
if ([views count] > 0) {
NSArray *sharedViews = [self sortViewsByTags:views];
_sharedElements = [self getSharedElementForCurrentTransition:sharedViews
withNewElements:YES
withOffsetX:0
withOffsetY:0];
[self resolveAnimationType:_sharedElements isInteractive:NO];
_isAsyncSharedTransitionConfigured = YES;
}
}
- (void)maybeRestartAnimationWithNewLayout
{
if ([_layoutedSharedViewsTags count] == 0 || [_currentSharedTransitionViews count] == 0) {
return;
}
NSMutableArray<REASharedElement *> *sharedElementToRestart = [NSMutableArray new];
for (REASharedElement *sharedElement in _sharedElements) {
NSNumber *viewTag = sharedElement.targetView.reactTag;
if ([_layoutedSharedViewsTags containsObject:viewTag] && _currentSharedTransitionViews[viewTag]) {
[sharedElementToRestart addObject:sharedElement];
}
}
for (REASharedElement *sharedElement in sharedElementToRestart) {
REAUIView *sourceView = sharedElement.sourceView;
REAUIView *targetView = sharedElement.targetView;
REASnapshot *newSourceViewSnapshot = [[REASnapshot alloc] initWithAbsolutePosition:sourceView];
REASnapshot *currentTargetViewSnapshot = _snapshotRegistry[targetView.reactTag];
REAFrame *frameData = _layoutedSharedViewsFrame[targetView.reactTag];
float currentOriginX = [currentTargetViewSnapshot.values[@"originX"] floatValue];
float currentOriginY = [currentTargetViewSnapshot.values[@"originY"] floatValue];
float currentOriginXByParent = [currentTargetViewSnapshot.values[@"originXByParent"] floatValue];
float currentOriginYByParent = [currentTargetViewSnapshot.values[@"originYByParent"] floatValue];
NSNumber *newOriginX = @(currentOriginX - currentOriginXByParent + frameData.x);
NSNumber *newOriginY = @(currentOriginY - currentOriginYByParent + frameData.y);
currentTargetViewSnapshot.values[@"width"] = @(frameData.width);
currentTargetViewSnapshot.values[@"height"] = @(frameData.height);
currentTargetViewSnapshot.values[@"originX"] = newOriginX;
currentTargetViewSnapshot.values[@"originY"] = newOriginY;
currentTargetViewSnapshot.values[@"globalOriginX"] = newOriginX;
currentTargetViewSnapshot.values[@"globalOriginY"] = newOriginY;
currentTargetViewSnapshot.values[@"originXByParent"] = @(frameData.x);
currentTargetViewSnapshot.values[@"originYByParent"] = @(frameData.y);
sharedElement.sourceViewSnapshot = newSourceViewSnapshot;
[self disableCleaningForViewTag:sourceView.reactTag];
[self disableCleaningForViewTag:targetView.reactTag];
}
[self startSharedTransition:sharedElementToRestart];
}
- (BOOL)configureAndStartSharedTransitionForViews:(NSArray<REAUIView *> *)views
isInteractive:(BOOL)isInteractive
withOffsetX:(double)offsetX
withOffsetY:(double)offsetY
{
NSArray *sharedViews = [self sortViewsByTags:views];
NSArray<REASharedElement *> *sharedElements = [self getSharedElementForCurrentTransition:sharedViews
withNewElements:NO
withOffsetX:offsetX
withOffsetY:offsetY];
if ([sharedElements count] == 0) {
return NO;
}
[self resolveAnimationType:sharedElements isInteractive:isInteractive];
[self configureTransitionContainer];
[self reparentSharedViewsForCurrentTransition:sharedElements];
[self startSharedTransition:sharedElements];
return YES;
}
- (NSArray *)sortViewsByTags:(NSArray *)views
{
/*
All shared views during the transition have the same parent. It is problematic if parent
view and their children are in the same transition. To keep the valid order in the z-axis,
we need to sort views by tags. Parent tag is lower than children tags.
*/
return [views sortedArrayUsingComparator:^NSComparisonResult(REAUIView *view1, REAUIView *view2) {
return [view2.reactTag compare:view1.reactTag];
}];
}
- (NSMutableArray<REASharedElement *> *)getSharedElementForCurrentTransition:(NSArray *)sharedViews
withNewElements:(BOOL)addedNewScreen
withOffsetX:(double)offsetX
withOffsetY:(double)offsetY
{
NSMutableArray<REAUIView *> *newTransitionViews = [NSMutableArray new];
NSMutableArray<REASharedElement *> *newSharedElements = [NSMutableArray new];
NSMutableSet<NSNumber *> *currentSharedViewsTags = [NSMutableSet new];
for (REAUIView *sharedView in sharedViews) {
[currentSharedViewsTags addObject:sharedView.reactTag];
}
for (REAUIView *sharedView in sharedViews) {
// add observers
REAUIView *sharedViewScreen = [REAScreensHelper getScreenForView:sharedView];
REAUIView *stack = [REAScreensHelper getStackForView:sharedViewScreen];
// find sibling for shared view
NSNumber *siblingViewTag = _findPrecedingViewTagForTransition(sharedView.reactTag);
REAUIView *siblingView = nil;
do {
siblingView = [_animationManager viewForTag:siblingViewTag];
if (siblingView == nil) {
[self clearAllSharedConfigsForViewTag:siblingViewTag];
siblingViewTag = _findPrecedingViewTagForTransition(sharedView.reactTag);
}
} while (siblingView == nil && siblingViewTag != nil);
if (siblingView == nil) {
// the sibling of shared view doesn't exist yet
continue;
}
REAUIView *viewSource;
REAUIView *viewTarget;
if (addedNewScreen) {
viewSource = siblingView;
viewTarget = sharedView;
} else {
viewSource = sharedView;
viewTarget = siblingView;
}
bool isInCurrentTransition = false;
if (_currentSharedTransitionViews[viewSource.reactTag] || _currentSharedTransitionViews[viewTarget.reactTag]) {
isInCurrentTransition = true;
if (addedNewScreen) {
siblingViewTag = _findPrecedingViewTagForTransition(siblingView.reactTag);
siblingView = [_animationManager viewForTag:siblingViewTag];
viewSource = siblingView;
viewTarget = sharedView;
}
}
if ([currentSharedViewsTags containsObject:viewSource.reactTag] &&
[currentSharedViewsTags containsObject:viewTarget.reactTag]) {
continue;
}
bool isModal = [REAScreensHelper isScreenModal:sharedViewScreen];
// check valid target screen configuration
int screensCount = [stack.reactSubviews count];
if (addedNewScreen && !isModal) {
// is under top
if (screensCount < 2) {
continue;
}
REAUIView *viewSourceParentScreen = [REAScreensHelper getScreenForView:viewSource];
REAUIView *screenUnderStackTop = stack.reactSubviews[screensCount - 2];
if (![screenUnderStackTop.reactTag isEqual:viewSourceParentScreen.reactTag] && !isInCurrentTransition) {
continue;
}
} else if (!addedNewScreen && !isModal) {
// is on top
REAUIView *viewTargetParentScreen = [REAScreensHelper getScreenForView:viewTarget];
// TODO macOS navigationController isn't available on macOS
#if !TARGET_OS_OSX
REAUIView *stackTarget = viewTargetParentScreen.reactViewController.navigationController.topViewController.view;
if (stackTarget != viewTargetParentScreen) {
continue;
}
#endif
}
if (isModal) {
[_viewsToHide addObject:viewSource.reactTag];
}
REASnapshot *sourceViewSnapshot;
if (!addedNewScreen) {
sourceViewSnapshot = _snapshotRegistry[viewSource.reactTag];
} else {
sourceViewSnapshot = [[REASnapshot alloc] initWithAbsolutePosition:viewSource];
}
if (addedNewScreen && !_currentSharedTransitionViews[viewSource.reactTag]) {
_snapshotRegistry[viewSource.reactTag] = sourceViewSnapshot;
}
REASnapshot *targetViewSnapshot;
if (addedNewScreen) {
targetViewSnapshot = [[REASnapshot alloc] initWithAbsolutePosition:viewTarget];
_snapshotRegistry[viewTarget.reactTag] = targetViewSnapshot;
} else {
targetViewSnapshot = [[REASnapshot alloc] initWithAbsolutePosition:viewTarget
withOffsetX:offsetX
withOffsetY:offsetY];
}
[newTransitionViews addObject:viewSource];
[newTransitionViews addObject:viewTarget];
REASharedElement *sharedElement = [[REASharedElement alloc] initWithSourceView:viewSource
sourceViewSnapshot:sourceViewSnapshot
targetView:viewTarget
targetViewSnapshot:targetViewSnapshot];
[newSharedElements addObject:sharedElement];
}
if ([newTransitionViews count] > 0) {
NSMutableArray *currentSourceViews = [NSMutableArray new];
for (REASharedElement *sharedElement in _sharedElements) {
[currentSourceViews addObject:sharedElement.sourceView];
}
NSMutableSet *newSourceViews = [NSMutableSet new];
for (REASharedElement *sharedElement in newSharedElements) {
[newSourceViews addObject:sharedElement.sourceView];
}
for (REAUIView *view in currentSourceViews) {
if (![newSourceViews containsObject:view]) {
_removedViewRegistry[view.reactTag] = view;
}
}
[_currentSharedTransitionViews removeAllObjects];
for (REAUIView *view in newTransitionViews) {
_currentSharedTransitionViews[view.reactTag] = view;
}
}
if ([newSharedElements count] != 0) {
_sharedElements = newSharedElements;
for (REASharedElement *sharedElement in newSharedElements) {
_sharedElementsLookup[sharedElement.sourceView.reactTag] = sharedElement;
}
}
return newSharedElements;
}
/*
Method swizzling is used to get notification from react-native-screens
about push or pop screen from stack.
*/
- (void)swizzleScreensMethods
{
#if LOAD_SCREENS_HEADERS
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL viewDidLayoutSubviewsSelector = @selector(viewDidLayoutSubviews);
SEL notifyWillDisappearSelector = @selector(notifyWillDisappear);
SEL viewIsAppearingSelector = @selector(viewIsAppearing:);
Class screenClass = [RNSScreen class];
Class screenViewClass = [RNSScreenView class];
BOOL allSelectorsAreAvailable = [RNSScreen instancesRespondToSelector:viewDidLayoutSubviewsSelector] &&
[RNSScreenView instancesRespondToSelector:notifyWillDisappearSelector] &&
[RNSScreen instancesRespondToSelector:viewIsAppearingSelector] &&
[RNSScreenView instancesRespondToSelector:@selector(isModal)]; // used by REAScreenHelper
if (allSelectorsAreAvailable) {
[REAUtils swizzleMethod:viewDidLayoutSubviewsSelector
forClass:screenClass
with:@selector(reanimated_viewDidLayoutSubviews)
fromClass:[self class]];
[REAUtils swizzleMethod:notifyWillDisappearSelector
forClass:screenViewClass
with:@selector(reanimated_notifyWillDisappear)
fromClass:[self class]];
[REAUtils swizzleMethod:viewIsAppearingSelector
forClass:screenClass
with:@selector(reanimated_viewIsAppearing:)
fromClass:[self class]];
_isConfigured = YES;
}
});
#endif
}
- (void)setDisappearingScreen:(REAUIView *)view
{
_disappearingScreen = view;
_isInteractive = [_sharedTransitionManager isInteractiveScreenChange:view];
}
- (REAUIView *)getDisappearingScreen
{
return _disappearingScreen;
}
- (void)setIsInteractive:(BOOL)isInteractive
{
_isInteractive = isInteractive;
}
- (BOOL)getIsInteractive
{
return _isInteractive;
}
- (void)reanimated_viewDidLayoutSubviews
{
// call original method from react-native-screens, self == RNScreen
[self reanimated_viewDidLayoutSubviews];
REAUIView *screen = [self valueForKey:@"screenView"];
[_sharedTransitionManager screenAddedToStack:screen];
}
- (void)reanimated_notifyWillDisappear
{
// call original method from react-native-screens, self == RNSScreenView
[self reanimated_notifyWillDisappear];
[_sharedTransitionManager makeSnapshotForScreenViews:(REAUIView *)self];
bool isModal = [REAScreensHelper isScreenModal:(REAUIView *)self];
if (isModal) {
[_sharedTransitionManager setIsInteractive:[_sharedTransitionManager isInteractiveScreenChange:(REAUIView *)self]];
[_sharedTransitionManager screenRemovedFromStack:(REAUIView *)self withOffsetX:0 withOffsetY:0];
} else {
[_sharedTransitionManager setDisappearingScreen:(REAUIView *)self];
}
}
- (void)reanimated_viewIsAppearing:(BOOL)animated
{
// call original method from react-native-screens, self == RNSScreen
[self reanimated_viewIsAppearing:animated];
REAUIView *disappearingScreen = [_sharedTransitionManager getDisappearingScreen];
REAUIView *targetScreen = [self valueForKey:@"screenView"];
if (disappearingScreen != NULL) {
[_sharedTransitionManager screenRemovedFromStack:disappearingScreen
withOffsetX:-targetScreen.superview.frame.origin.x
withOffsetY:-targetScreen.superview.frame.origin.y];
}
[_sharedTransitionManager setDisappearingScreen:NULL];
}
- (void)screenAddedToStack:(REAUIView *)screen
{
if (screen.superview != nil) {
[self runAsyncSharedTransition];
}
}
- (void)screenRemovedFromStack:(REAUIView *)screen withOffsetX:(double)offsetX withOffsetY:(double)offsetY
{
_isStackDropped = NO;
REAUIView *stack = [REAScreensHelper getStackForView:screen];
bool isModal = [REAScreensHelper isScreenModal:screen];
bool isRemovedInParentStack = [self isRemovedFromHigherStack:screen];
bool isInteractive = [self getIsInteractive];
if ((stack != nil || isModal) && !isRemovedInParentStack) {
// screen is removed from React tree (navigation.navigate(<screenName>))
bool isScreenRemovedFromReactTree = [self isScreen:screen outsideStack:stack];
// click on button goBack on native header
bool isTriggeredByGoBackButton = [self isScreen:screen onTopOfStack:stack];
bool shouldRunTransition = (isScreenRemovedFromReactTree || isTriggeredByGoBackButton) &&
!(isInteractive && [_currentSharedTransitionViews count] > 0);
if (shouldRunTransition) {
[self runSharedTransitionForSharedViewsOnScreen:screen
isInteractive:isInteractive
withOffsetX:offsetX
withOffsetY:offsetY];
} else {
[self makeSnapshotForScreenViews:screen];
}
} else {
// removed stack
if (!isInteractive) {
[self clearConfigForStackNow:stack];
} else {
_isStackDropped = YES;
}
}
}
- (bool)isInteractiveScreenChange:(REAUIView *)screen
{
#if !TARGET_OS_OSX
return screen.reactViewController.transitionCoordinator.interactive;
#else
// TODO macOS transitionCoordinator isn't available on macOS
return false;
#endif
}
- (void)makeSnapshotForScreenViews:(REAUIView *)screen
{
REANodeFind(screen, ^int(id<RCTComponent> view) {
NSNumber *viewTag = view.reactTag;
if (self->_currentSharedTransitionViews[viewTag]) {
return false;
}
if ([self->_animationManager hasAnimationForTag:viewTag type:SHARED_ELEMENT_TRANSITION]) {
REASnapshot *snapshot = [[REASnapshot alloc] initWithAbsolutePosition:(REAUIView *)view
withOffsetX:0
withOffsetY:0];
self->_snapshotRegistry[viewTag] = snapshot;
}
return false;
});
}
- (void)clearConfigForStackNow:(REAUIView *)stack
{
for (REAUIView *screen in stack.reactSubviews) {
[self clearConfigForScreen:screen];
}
}
- (BOOL)isScreen:(REAUIView *)screen outsideStack:(REAUIView *)stack
{
for (REAUIView *child in stack.reactSubviews) {
if ([child.reactTag isEqual:screen.reactTag]) {
return NO;
}
}
return YES;
}
- (BOOL)isScreen:(REAUIView *)screen onTopOfStack:(REAUIView *)stack
{
int screenCount = stack.reactSubviews.count;
return screenCount > 0 && screen == stack.reactSubviews.lastObject;
}
- (BOOL)isRemovedFromHigherStack:(REAUIView *)screen
{
REAUIView *stack = screen.reactSuperview;
while (stack != nil) {
#if !TARGET_OS_OSX
screen = stack.reactViewController.navigationController.topViewController.view;
#else
// TODO macOS navigationController isn't available on macOS
screen = nil;
#endif
if (screen == nil) {
break;
}
if (screen.superview == nil) {
return YES;
}
stack = screen.reactSuperview;
}
return NO;
}
- (void)runSharedTransitionForSharedViewsOnScreen:(REAUIView *)screen
isInteractive:(BOOL)isInteractive
withOffsetX:(double)offsetX
withOffsetY:(double)offsetY
{
NSMutableArray<REAUIView *> *removedViews = [NSMutableArray new];
REANodeFind(screen, ^int(id<RCTComponent> view) {
if ([self->_animationManager hasAnimationForTag:view.reactTag type:SHARED_ELEMENT_TRANSITION]) {
[removedViews addObject:(REAUIView *)view];
}
return false;
});
BOOL startedAnimation = [self configureAndStartSharedTransitionForViews:removedViews
isInteractive:isInteractive
withOffsetX:offsetX
withOffsetY:offsetY];
if (startedAnimation) {
_removedViews = removedViews;
} else if (![self isInteractiveScreenChange:screen]) {
[self clearConfigForScreen:screen];
} else {
_clearScreen = YES;
}
}
- (void)runAsyncSharedTransition
{
if ([_sharedElements count] == 0 || !_isAsyncSharedTransitionConfigured) {
return;
}
for (REASharedElement *sharedElement in _sharedElements) {
REAUIView *viewTarget = sharedElement.targetView;
REASnapshot *targetViewSnapshot = [[REASnapshot alloc] initWithAbsolutePosition:viewTarget];
_snapshotRegistry[viewTarget.reactTag] = targetViewSnapshot;
sharedElement.targetViewSnapshot = targetViewSnapshot;
}
[self configureTransitionContainer];
[self reparentSharedViewsForCurrentTransition:_sharedElements];
[self startSharedTransition:_sharedElements];
[_addedSharedViews removeAllObjects];
_isAsyncSharedTransitionConfigured = NO;
}
- (void)configureTransitionContainer
{
if (!_isSharedTransitionActive) {
_isSharedTransitionActive = YES;
#if TARGET_OS_OSX
REAUIView *mainWindow = UIApplication.sharedApplication.keyWindow;
#else
REAUIView *mainWindow = (REAUIView *)RCTKeyWindow();
#endif
if (_transitionContainer == nil) {
_transitionContainer = [REAUIView new];
}
[mainWindow addSubview:_transitionContainer];
// TODO macOS bringSubviewToFront isn't available on macOS
#if !TARGET_OS_OSX
[mainWindow bringSubviewToFront:_transitionContainer];
#endif
}
}
- (void)reparentSharedViewsForCurrentTransition:(NSArray *)sharedElements
{
for (REASharedElement *sharedElement in sharedElements) {
REAUIView *viewSource = sharedElement.sourceView;
[_reattachedViews addObject:viewSource];
if (_sharedTransitionParent[viewSource.reactTag] == nil) {
_sharedTransitionParent[viewSource.reactTag] = viewSource.superview;
_sharedTransitionInParentIndex[viewSource.reactTag] = @([viewSource.superview.subviews indexOfObject:viewSource]);
[viewSource removeFromSuperview];
[_transitionContainer addSubview:viewSource];
}
}
}
- (void)startSharedTransition:(NSArray *)sharedElements
{
for (REASharedElement *sharedElement in sharedElements) {
sharedElement.targetView.hidden = YES;
LayoutAnimationType type = sharedElement.animationType;
[self onViewTransition:sharedElement.sourceView
before:sharedElement.sourceViewSnapshot
after:sharedElement.targetViewSnapshot
type:type];
}
}
- (void)onViewTransition:(REAUIView *)view
before:(REASnapshot *)before
after:(REASnapshot *)after
type:(LayoutAnimationType)type
{
NSMutableDictionary *targetValues = after.values;
NSMutableDictionary *currentValues = before.values;
// TODO macOS bringSubviewToFront isn't available on macOS
#if !TARGET_OS_OSX
[view.superview bringSubviewToFront:view];
#endif
NSDictionary *preparedValues = [self prepareDataForWorklet:currentValues targetValues:targetValues];
[_animationManager startAnimationForTag:view.reactTag type:type yogaValues:preparedValues];
}
- (void)finishSharedAnimation:(REAUIView *)view removeView:(BOOL)removeView
{
if (!_isConfigured) {
return;
}
NSNumber *viewTag = view.reactTag;
if (_disableCleaningForView[viewTag]) {
[self enableCleaningForViewTag:viewTag];
return;
}
REASharedElement *sharedElement = _sharedElementsLookup[viewTag];
if (sharedElement == nil) {
return;
}
[_sharedElementsLookup removeObjectForKey:viewTag];
if ([_reattachedViews containsObject:view]) {
[_reattachedViews removeObject:view];
[view removeFromSuperview];
REAUIView *parent = _sharedTransitionParent[viewTag];
int childIndex = [_sharedTransitionInParentIndex[viewTag] intValue];
REAUIView *screen = [REAScreensHelper getScreenForView:parent];
bool isScreenInReactTree = screen.reactSuperview != nil;
if (isScreenInReactTree) {
[parent insertSubview:view atIndex:childIndex];
REASnapshot *viewSourcePreviousSnapshot = _snapshotRegistry[viewTag];
[_animationManager progressLayoutAnimationWithStyle:viewSourcePreviousSnapshot.values
forTag:viewTag
isSharedTransition:YES];
float originXByParent = [viewSourcePreviousSnapshot.values[@"originXByParent"] floatValue];
float originYByParent = [viewSourcePreviousSnapshot.values[@"originYByParent"] floatValue];
float height = [viewSourcePreviousSnapshot.values[@"height"] floatValue];
float width = [viewSourcePreviousSnapshot.values[@"width"] floatValue];
[view setCenter:CGPointMake(originXByParent + width / 2.0, originYByParent + height / 2.0)];
}
[_sharedTransitionParent removeObjectForKey:viewTag];
[_sharedTransitionInParentIndex removeObjectForKey:viewTag];
}
REAUIView *targetView = sharedElement.targetView;
targetView.hidden = NO;
if ([_viewsToHide containsObject:viewTag]) {
view.hidden = YES;
}
if (!removeView) {
[_removedViews removeObject:view];
}
if ([_removedViews containsObject:view]) {
[_animationManager clearSharedTransitionConfigForTag:viewTag];
}
if (_removedViewRegistry[view.reactTag]) {
return;
}
if ([_reattachedViews count] == 0) {
[_transitionContainer removeFromSuperview];
[_removedViewRegistry removeAllObjects];
[_currentSharedTransitionViews removeAllObjects];
[_removedViews removeAllObjects];
[_sharedElements removeAllObjects];
[_sharedElementsLookup removeAllObjects];
[_viewsToHide removeAllObjects];
_isSharedTransitionActive = NO;
}
}
- (void)setFindPrecedingViewTagForTransitionBlock:
(REAFindPrecedingViewTagForTransitionBlock)findPrecedingViewTagForTransition
{
_findPrecedingViewTagForTransition = findPrecedingViewTagForTransition;
}
- (void)setCancelAnimationBlock:(REACancelAnimationBlock)cancelAnimationBlock
{
_cancelLayoutAnimation = cancelAnimationBlock;
}
- (void)clearAllSharedConfigsForViewTag:(NSNumber *)viewTag
{
if (viewTag != nil) {
[_snapshotRegistry removeObjectForKey:viewTag];
[_animationManager clearSharedTransitionConfigForTag:viewTag];
}
}
- (void)cancelAnimation:(NSNumber *)viewTag
{
_cancelLayoutAnimation(viewTag);
}
- (void)disableCleaningForViewTag:(NSNumber *)viewTag
{
NSNumber *counter = _disableCleaningForView[viewTag];
if (counter != nil) {
_disableCleaningForView[viewTag] = @([counter intValue] + 1);
} else {
_disableCleaningForView[viewTag] = @(1);
}
}
- (void)enableCleaningForViewTag:(NSNumber *)viewTag
{
NSNumber *counter = _disableCleaningForView[viewTag];
if (counter == nil) {
return;
}
int counterInt = [counter intValue];
if (counterInt == 1) {
[_disableCleaningForView removeObjectForKey:viewTag];
} else {
_disableCleaningForView[viewTag] = @(counterInt - 1);
}
}
- (void)resolveAnimationType:(NSArray<REASharedElement *> *)sharedElements isInteractive:(BOOL)isInteractive
{
for (REASharedElement *sharedElement in sharedElements) {
NSNumber *viewTag = sharedElement.sourceView.reactTag;
bool viewHasProgressAnimation = [self->_animationManager hasAnimationForTag:viewTag
type:SHARED_ELEMENT_TRANSITION_PROGRESS];
if (viewHasProgressAnimation || isInteractive) {
sharedElement.animationType = SHARED_ELEMENT_TRANSITION_PROGRESS;
} else {
sharedElement.animationType = SHARED_ELEMENT_TRANSITION;
}
}
}
- (NSDictionary *)prepareDataForWorklet:(NSMutableDictionary *)currentValues
targetValues:(NSMutableDictionary *)targetValues
{
NSMutableDictionary *workletValues = [_animationManager prepareDataForLayoutAnimatingWorklet:currentValues
targetValues:targetValues];
workletValues[@"currentTransformMatrix"] = currentValues[@"combinedTransformMatrix"];
workletValues[@"targetTransformMatrix"] = targetValues[@"combinedTransformMatrix"];
workletValues[@"currentBorderRadius"] = currentValues[@"borderRadius"];
workletValues[@"targetBorderRadius"] = targetValues[@"borderRadius"];
return workletValues;
}
- (void)onScreenRemoval:(REAUIView *)screen stack:(REAUIView *)stack
{
if (_isStackDropped && screen != nil) {
// to clear config from stack after swipe back
[self clearConfigForStackNow:stack];
_isStackDropped = NO;
} else if (_clearScreen) {
// to clear config from screen after swipe back
[self clearConfigForScreen:screen];
_clearScreen = NO;
}
}
- (void)clearConfigForScreen:(REAUIView *)screen
{
REANodeFind(screen, ^int(id<RCTComponent> _Nonnull view) {
[self clearAllSharedConfigsForViewTag:view.reactTag];
return false;
});
}
@end

View File

@@ -0,0 +1,16 @@
#import <Foundation/Foundation.h>
#import <RNReanimated/REAUIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface REASnapshot : NSObject
@property NSMutableDictionary *values;
- (instancetype)init:(REAUIView *)view;
- (instancetype)initWithAbsolutePosition:(REAUIView *)view;
- (instancetype)initWithAbsolutePosition:(REAUIView *)view withOffsetX:(double)offsetX withOffsetY:(double)offsetY;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,180 @@
#import <Foundation/Foundation.h>
#import <RNReanimated/REAScreensHelper.h>
#import <RNReanimated/REASnapshot.h>
#import <React/RCTUtils.h>
#import <React/RCTView.h>
#import <React/UIView+React.h>
NS_ASSUME_NONNULL_BEGIN
@implementation REASnapshot
const int ScreenStackPresentationModal = 1; // RNSScreenStackPresentationModal
const int DEFAULT_MODAL_TOP_OFFSET = 69; // Default iOS modal is shifted from screen top edge by 69px
- (instancetype)init:(REAUIView *)view
{
self = [super init];
[self makeSnapshotForView:view useAbsolutePositionOnly:NO withOffsetX:0 withOffsetY:0];
return self;
}
- (instancetype)initWithAbsolutePosition:(REAUIView *)view withOffsetX:(double)offsetX withOffsetY:(double)offsetY
{
self = [super init];
[self makeSnapshotForView:view useAbsolutePositionOnly:YES withOffsetX:offsetX withOffsetY:offsetY];
return self;
}
- (instancetype)initWithAbsolutePosition:(REAUIView *)view
{
self = [super init];
[self makeSnapshotForView:view useAbsolutePositionOnly:YES withOffsetX:0 withOffsetY:0];
return self;
}
- (void)makeSnapshotForView:(REAUIView *)view
useAbsolutePositionOnly:(BOOL)useAbsolutePositionOnly
withOffsetX:(double)offsetX
withOffsetY:(double)offsetY
{
#if TARGET_OS_OSX
REAUIView *mainWindow = UIApplication.sharedApplication.keyWindow;
#else
REAUIView *mainWindow = RCTKeyWindow();
#endif
CGPoint absolutePosition = [[view superview] convertPoint:view.center toView:mainWindow];
_values = [NSMutableDictionary new];
#if TARGET_OS_OSX
_values[@"windowWidth"] = [NSNumber numberWithDouble:mainWindow.frame.size.width];
_values[@"windowHeight"] = [NSNumber numberWithDouble:mainWindow.frame.size.height];
#else
_values[@"windowWidth"] = [NSNumber numberWithDouble:mainWindow.bounds.size.width];
_values[@"windowHeight"] = [NSNumber numberWithDouble:mainWindow.bounds.size.height];
#endif
_values[@"width"] = [NSNumber numberWithDouble:(double)(view.bounds.size.width)];
_values[@"height"] = [NSNumber numberWithDouble:(double)(view.bounds.size.height)];
_values[@"globalOriginX"] = [NSNumber numberWithDouble:offsetX + absolutePosition.x - view.bounds.size.width / 2.0];
_values[@"globalOriginY"] = [NSNumber numberWithDouble:offsetY + absolutePosition.y - view.bounds.size.height / 2.0];
if (useAbsolutePositionOnly) {
_values[@"originX"] = _values[@"globalOriginX"];
_values[@"originY"] = _values[@"globalOriginY"];
_values[@"originXByParent"] = [NSNumber numberWithDouble:view.center.x - view.bounds.size.width / 2.0];
_values[@"originYByParent"] = [NSNumber numberWithDouble:view.center.y - view.bounds.size.height / 2.0];
#if TARGET_OS_OSX
REAUIView *header = nil;
#else
REAUIView *navigationContainer = view.reactViewController.navigationController.view;
REAUIView *header = [navigationContainer.subviews count] > 1 ? navigationContainer.subviews[1] : nil;
#endif
if (header != nil) {
CGFloat headerHeight = header.frame.size.height;
CGFloat headerOriginY = header.frame.origin.y;
REAUIView *screen = [REAScreensHelper getScreenForView:view];
if ([REAScreensHelper isScreenModal:screen] && screen.superview == nil) {
int additionalModalOffset = 0;
REAUIView *screenWrapper = [REAScreensHelper getScreenWrapper:view];
int screenType = [REAScreensHelper getScreenType:screenWrapper];
if (screenType == ScreenStackPresentationModal) {
additionalModalOffset = DEFAULT_MODAL_TOP_OFFSET;
}
float originY = [_values[@"originY"] doubleValue] + headerHeight + headerOriginY + additionalModalOffset;
_values[@"originY"] = @(originY);
}
_values[@"headerHeight"] = @(headerHeight);
} else {
_values[@"headerHeight"] = @(0);
}
// store the transofrmMatrix of this view, so that we can reestablish it later
CGAffineTransform transform = view.transform;
_values[@"transformMatrix"] = @[
@(transform.a),
@(transform.b),
@(0),
@(transform.c),
@(transform.d),
@(0),
@(transform.tx),
@(transform.ty),
@(1)
];
transform = [self findCombinedTransform:view];
_values[@"combinedTransformMatrix"] = @[
@(transform.a),
@(transform.b),
@(0),
@(transform.c),
@(transform.d),
@(0),
@(transform.tx),
@(transform.ty),
@(1)
];
REAUIView *transformedView = [self maybeFindTransitionView:view];
if (transformedView != nil) {
// iOS affine matrix: https://developer.apple.com/documentation/corefoundation/cgaffinetransform
transform = transformedView.transform;
// revert the transformation that was applied to the view when transition started, since we are intereseted only
// in the final result of the transition
CGPoint center = [[view superview] convertPoint:view.center toView:transformedView.superview];
CGPoint parentCenter = transformedView.center;
CGFloat x = center.x, y = center.y, a = transform.a, b = transform.b, c = transform.c, d = transform.d,
tx = transform.tx, ty = transform.ty, parentX = parentCenter.x, parentY = parentCenter.y;
center.x = (b - a) * (x - parentX - tx) / (b * c - a * d) + parentX;
center.y = (d - c) * (y - parentY - ty) / (a * d - b * c) + parentY;
CGPoint absolute = [[transformedView superview] convertPoint:center toView:nil];
_values[@"originX"] = [NSNumber numberWithDouble:offsetX + absolute.x - view.bounds.size.width / 2.0];
_values[@"originY"] = [NSNumber numberWithDouble:offsetY + absolute.y - view.bounds.size.height / 2.0];
}
#if defined(RCT_NEW_ARCH_ENABLED) || TARGET_OS_TV
_values[@"borderRadius"] = @(0);
#else
if ([view respondsToSelector:@selector(borderRadius)]) {
// For example `RCTTextView` doesn't have `borderRadius` selector
_values[@"borderRadius"] = @(((RCTView *)view).borderRadius);
} else {
_values[@"borderRadius"] = @(0);
}
#endif
} else {
_values[@"originX"] = @(view.center.x - view.bounds.size.width / 2.0);
_values[@"originY"] = @(view.center.y - view.bounds.size.height / 2.0);
}
}
- (CGAffineTransform)findCombinedTransform:(REAUIView *)view
{
CGAffineTransform transform = view.transform;
view = view.superview;
while (view != nil && ![REAScreensHelper isRNSScreenType:view]) {
// ignore transforms that are caused by transitions
if (![view.superview isKindOfClass:NSClassFromString(@"UITransitionView")]) {
CGAffineTransform t = view.transform;
// we are ignoring translations in superviews, since the positioning obtained by converting the center point (and
// applying it's view transforms) to the main window is already correct, here we only care about scale, shear and
// rotation
transform = CGAffineTransformConcat(transform, CGAffineTransformMake(t.a, t.b, t.c, t.d, 0, 0));
}
view = view.superview;
}
return transform;
}
- (REAUIView *)maybeFindTransitionView:(REAUIView *)view
{
while (view != nil && ![REAScreensHelper isRNSScreenType:view]) {
if ([view.superview isKindOfClass:NSClassFromString(@"UITransitionView")]) {
return view;
}
view = view.superview;
}
return nil;
}
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,6 @@
#import <RNReanimated/REAAnimationsManager.h>
@interface REASwizzledUIManager : NSObject
- (instancetype)initWithUIManager:(RCTUIManager *)uiManager
withAnimationManager:(REAAnimationsManager *)animationsManager;
@end

View File

@@ -0,0 +1,382 @@
#import <RNReanimated/FeaturesConfig.h>
#import <RNReanimated/REASwizzledUIManager.h>
#import <RNReanimated/REAUIKit.h>
#import <RNReanimated/REAUtils.h>
#import <React/RCTLayoutAnimation.h>
#import <React/RCTLayoutAnimationGroup.h>
#import <React/RCTRootShadowView.h>
#import <React/RCTRootViewInternal.h>
#import <React/RCTUIManager.h>
#import <React/RCTUIManagerUtils.h>
#import <objc/runtime.h>
@interface RCTUIManager (Reanimated)
@property REAAnimationsManager *animationsManager;
- (NSArray<id<RCTComponent>> *)_childrenToRemoveFromContainer:(id<RCTComponent>)container
atIndices:(NSArray<NSNumber *> *)atIndices;
@end
@implementation RCTUIManager (Reanimated)
@dynamic animationsManager;
- (void)setAnimationsManager:(REAAnimationsManager *)animationsManager
{
objc_setAssociatedObject(self, @selector(animationsManager), animationsManager, OBJC_ASSOCIATION_RETAIN);
}
- (id)animationsManager
{
return objc_getAssociatedObject(self, @selector(animationsManager));
}
@end
@implementation REASwizzledUIManager
std::atomic<uint> isFlushingBlocks;
std::atomic<bool> hasPendingBlocks;
- (instancetype)initWithUIManager:(RCTUIManager *)uiManager
withAnimationManager:(REAAnimationsManager *)animationsManager
{
if (self = [super init]) {
isFlushingBlocks = 0;
hasPendingBlocks = false;
[uiManager setAnimationsManager:animationsManager];
[self swizzleMethods];
IMP isExecutingUpdatesBatchImpl = imp_implementationWithBlock(^() {
return hasPendingBlocks || isFlushingBlocks > 0;
});
class_addMethod([RCTUIManager class], @selector(isExecutingUpdatesBatch), isExecutingUpdatesBatchImpl, "");
}
return self;
}
- (void)swizzleMethods
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[REAUtils swizzleMethod:@selector(uiBlockWithLayoutUpdateForRootView:)
forClass:[RCTUIManager class]
with:@selector(reanimated_uiBlockWithLayoutUpdateForRootView:)
fromClass:[self class]];
SEL manageChildrenOriginal = @selector
(_manageChildren:moveFromIndices:moveToIndices:addChildReactTags:addAtIndices:removeAtIndices:registry:);
SEL manageChildrenReanimated =
@selector(reanimated_manageChildren:
moveFromIndices:moveToIndices:addChildReactTags:addAtIndices:removeAtIndices:registry:);
[REAUtils swizzleMethod:manageChildrenOriginal
forClass:[RCTUIManager class]
with:manageChildrenReanimated
fromClass:[self class]];
[REAUtils swizzleMethod:@selector(addUIBlock:)
forClass:[RCTUIManager class]
with:@selector(reanimated_addUIBlock:)
fromClass:[self class]];
[REAUtils swizzleMethod:@selector(prependUIBlock:)
forClass:[RCTUIManager class]
with:@selector(reanimated_prependUIBlock:)
fromClass:[self class]];
[REAUtils swizzleMethod:@selector(flushUIBlocksWithCompletion:)
forClass:[RCTUIManager class]
with:@selector(reanimated_flushUIBlocksWithCompletion:)
fromClass:[self class]];
});
}
- (void)reanimated_manageChildren:(NSNumber *)containerTag
moveFromIndices:(NSArray<NSNumber *> *)moveFromIndices
moveToIndices:(NSArray<NSNumber *> *)moveToIndices
addChildReactTags:(NSArray<NSNumber *> *)addChildReactTags
addAtIndices:(NSArray<NSNumber *> *)addAtIndices
removeAtIndices:(NSArray<NSNumber *> *)removeAtIndices
registry:(NSMutableDictionary<NSNumber *, id<RCTComponent>> *)registry
{
bool isLayoutAnimationEnabled = reanimated::FeaturesConfig::isLayoutAnimationEnabled();
id<RCTComponent> container;
NSArray<id<RCTComponent>> *permanentlyRemovedChildren;
BOOL containerIsRootOfViewController = NO;
RCTUIManager *originalSelf = (RCTUIManager *)self;
if (isLayoutAnimationEnabled) {
container = registry[containerTag];
permanentlyRemovedChildren = [originalSelf _childrenToRemoveFromContainer:container atIndices:removeAtIndices];
if ([container isKindOfClass:[REAUIView class]]) {
UIViewController *controller = ((REAUIView *)container).reactViewController;
UIViewController *parentController = ((REAUIView *)container).superview.reactViewController;
containerIsRootOfViewController = controller != parentController;
}
// we check if the container we`re removing from is a root view
// of some view controller. In that case, we skip running exiting animations
// in its children, to prevent issues with RN Screens.
if (containerIsRootOfViewController) {
NSArray<id<RCTComponent>> *permanentlyRemovedChildren =
[originalSelf _childrenToRemoveFromContainer:container atIndices:removeAtIndices];
for (REAUIView *view in permanentlyRemovedChildren) {
[originalSelf.animationsManager endAnimationsRecursive:view];
[originalSelf.animationsManager removeAnimationsFromSubtree:view];
}
[originalSelf.animationsManager onScreenRemoval:(REAUIView *)permanentlyRemovedChildren[0]
stack:(REAUIView *)container];
}
}
// call original method
[self reanimated_manageChildren:containerTag
moveFromIndices:moveFromIndices
moveToIndices:moveToIndices
addChildReactTags:addChildReactTags
addAtIndices:addAtIndices
removeAtIndices:removeAtIndices
registry:registry];
if (!isLayoutAnimationEnabled) {
return;
}
if (containerIsRootOfViewController) {
return;
}
// we sort the (index, view) pairs to make sure we insert views back in order
NSMutableArray<NSArray<id> *> *removedViewsWithIndices = [NSMutableArray new];
for (int i = 0; i < removeAtIndices.count; i++) {
removedViewsWithIndices[i] = @[ removeAtIndices[i], permanentlyRemovedChildren[i] ];
}
[removedViewsWithIndices
sortUsingComparator:^NSComparisonResult(NSArray<id> *_Nonnull obj1, NSArray<id> *_Nonnull obj2) {
return [(NSNumber *)obj1[0] compare:(NSNumber *)obj2[0]];
}];
[originalSelf.animationsManager reattachAnimatedChildren:permanentlyRemovedChildren
toContainer:container
atIndices:removeAtIndices];
}
- (RCTViewManagerUIBlock)reanimated_uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView *)rootShadowView
{
if (!reanimated::FeaturesConfig::isLayoutAnimationEnabled()) {
return [self reanimated_uiBlockWithLayoutUpdateForRootView:rootShadowView];
}
RCTUIManager *originalSelf = (RCTUIManager *)self;
#if REACT_NATIVE_MINOR_VERSION >= 73
NSPointerArray *affectedShadowViews = [NSPointerArray weakObjectsPointerArray];
#else
NSHashTable<RCTShadowView *> *affectedShadowViews = [NSHashTable weakObjectsHashTable];
#endif
[rootShadowView layoutWithAffectedShadowViews:affectedShadowViews];
if (!affectedShadowViews.count) {
// no frame change results in no UI update block
return nil;
}
typedef struct {
CGRect frame;
UIUserInterfaceLayoutDirection layoutDirection;
BOOL isNew;
BOOL parentIsNew;
RCTDisplayType displayType;
} RCTFrameData;
// Construct arrays then hand off to main thread
NSUInteger count = affectedShadowViews.count;
NSMutableArray *reactTags = [[NSMutableArray alloc] initWithCapacity:count];
NSMutableData *framesData = [[NSMutableData alloc] initWithLength:sizeof(RCTFrameData) * count];
{
NSUInteger index = 0;
RCTFrameData *frameDataArray = (RCTFrameData *)framesData.mutableBytes;
for (RCTShadowView *shadowView in affectedShadowViews) {
reactTags[index] = shadowView.reactTag;
RCTLayoutMetrics layoutMetrics = shadowView.layoutMetrics;
frameDataArray[index++] = (RCTFrameData){
layoutMetrics.frame,
layoutMetrics.layoutDirection,
shadowView.isNewView,
shadowView.superview.isNewView,
layoutMetrics.displayType};
}
}
for (RCTShadowView *shadowView in affectedShadowViews) {
// We have to do this after we build the parentsAreNew array.
shadowView.newView = NO;
NSNumber *reactTag = shadowView.reactTag;
if (shadowView.onLayout) {
CGRect frame = shadowView.layoutMetrics.frame;
shadowView.onLayout(@{
@"layout" : @{
@"x" : @(frame.origin.x),
@"y" : @(frame.origin.y),
@"width" : @(frame.size.width),
@"height" : @(frame.size.height),
},
});
}
if (RCTIsReactRootView(reactTag) && [shadowView isKindOfClass:[RCTRootShadowView class]]) {
CGSize contentSize = shadowView.layoutMetrics.frame.size;
RCTExecuteOnMainQueue(^{
REAUIView *view = [originalSelf viewForReactTag:(NSNumber *)reactTag];
RCTAssert(view != nil, @"view (for ID %@) not found", reactTag);
RCTRootView *rootView = (RCTRootView *)[view superview];
if ([rootView isKindOfClass:[RCTRootView class]]) {
rootView.intrinsicContentSize = contentSize;
}
});
}
}
// Perform layout (possibly animated)
return ^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, REAUIView *> *viewRegistry) {
const RCTFrameData *frameDataArray = (const RCTFrameData *)framesData.bytes;
RCTLayoutAnimationGroup *layoutAnimationGroup = [uiManager valueForKey:@"_layoutAnimationGroup"];
__block NSUInteger completionsCalled = 0;
NSMutableDictionary<NSNumber *, REASnapshot *> *snapshotsBefore = [NSMutableDictionary dictionary];
NSInteger index = 0;
for (NSNumber *reactTag in reactTags) {
RCTFrameData frameData = frameDataArray[index++];
REAUIView *view = [originalSelf viewForReactTag:(NSNumber *)reactTag];
CGRect frame = frameData.frame;
UIUserInterfaceLayoutDirection layoutDirection = frameData.layoutDirection;
BOOL isNew = frameData.isNew;
RCTLayoutAnimation *updatingLayoutAnimation = isNew ? nil : layoutAnimationGroup.updatingLayoutAnimation;
BOOL shouldAnimateCreation = isNew && !frameData.parentIsNew;
RCTLayoutAnimation *creatingLayoutAnimation =
shouldAnimateCreation ? layoutAnimationGroup.creatingLayoutAnimation : nil;
BOOL isHidden = frameData.displayType == RCTDisplayTypeNone;
void (^completion)(BOOL) = ^(BOOL finished) {
completionsCalled++;
if (layoutAnimationGroup.callback && completionsCalled == count) {
layoutAnimationGroup.callback(@[ @(finished) ]);
// It's unsafe to call this callback more than once, so we nil it out here
// to make sure that doesn't happen.
layoutAnimationGroup.callback = nil;
}
};
if (view.reactLayoutDirection != layoutDirection) {
view.reactLayoutDirection = layoutDirection;
}
if (view.isHidden != isHidden) {
view.hidden = isHidden;
}
// Reanimated changes /start
REASnapshot *snapshotBefore =
isNew ? nil : [originalSelf.animationsManager prepareSnapshotBeforeMountForView:view];
snapshotsBefore[reactTag] = snapshotBefore;
// Reanimated changes /end
if (creatingLayoutAnimation) {
// Animate view creation
[view reactSetFrame:frame];
CATransform3D finalTransform = view.layer.transform;
CGFloat finalOpacity = view.layer.opacity;
NSString *property = creatingLayoutAnimation.property;
if ([property isEqualToString:@"scaleXY"]) {
view.layer.transform = CATransform3DMakeScale(0, 0, 0);
} else if ([property isEqualToString:@"scaleX"]) {
view.layer.transform = CATransform3DMakeScale(0, 1, 0);
} else if ([property isEqualToString:@"scaleY"]) {
view.layer.transform = CATransform3DMakeScale(1, 0, 0);
} else if ([property isEqualToString:@"opacity"]) {
view.layer.opacity = 0.0;
} else {
RCTLogError(@"Unsupported layout animation createConfig property %@", creatingLayoutAnimation.property);
}
[creatingLayoutAnimation
performAnimations:^{
if ([property isEqualToString:@"scaleX"] || [property isEqualToString:@"scaleY"] ||
[property isEqualToString:@"scaleXY"]) {
view.layer.transform = finalTransform;
} else if ([property isEqualToString:@"opacity"]) {
view.layer.opacity = finalOpacity;
}
}
withCompletionBlock:completion];
} else if (updatingLayoutAnimation) {
// Animate view update
[updatingLayoutAnimation
performAnimations:^{
[view reactSetFrame:frame];
}
withCompletionBlock:completion];
} else {
// Update without animation
[view reactSetFrame:frame];
completion(YES);
}
}
// Reanimated changes /start
index = 0;
for (NSNumber *reactTag in reactTags) {
RCTFrameData frameData = frameDataArray[index++];
REAUIView *view = [originalSelf viewForReactTag:(NSNumber *)reactTag];
BOOL isNew = frameData.isNew;
CGRect frame = frameData.frame;
REASnapshot *snapshotBefore = snapshotsBefore[reactTag];
if (isNew || snapshotBefore != nil) {
[originalSelf.animationsManager viewDidMount:view withBeforeSnapshot:snapshotBefore withNewFrame:frame];
}
}
// Clean up
// below line serves as this one uiManager->_layoutAnimationGroup = nil;, because we don't have access to the
// private field
[uiManager setValue:nil forKey:@"_layoutAnimationGroup"];
[originalSelf.animationsManager viewsDidLayout];
// Reanimated changes /end
};
}
- (void)reanimated_addUIBlock:(RCTViewManagerUIBlock)block
{
RCTAssertUIManagerQueue();
hasPendingBlocks = true;
[self reanimated_addUIBlock:block];
}
- (void)reanimated_prependUIBlock:(RCTViewManagerUIBlock)block
{
RCTAssertUIManagerQueue();
hasPendingBlocks = true;
[self reanimated_prependUIBlock:block];
}
- (void)reanimated_flushUIBlocksWithCompletion:(void (^)(void))completion
{
RCTAssertUIManagerQueue();
if (hasPendingBlocks) {
++isFlushingBlocks;
hasPendingBlocks = false;
[self reanimated_addUIBlock:^(
__unused RCTUIManager *manager, __unused NSDictionary<NSNumber *, REAUIView *> *viewRegistry) {
--isFlushingBlocks;
}];
}
[self reanimated_flushUIBlocksWithCompletion:completion];
}
@end