Files
Eric FELIXINE e30ae8ed09 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
2026-06-01 18:00:35 -04:00

824 lines
31 KiB
Objective-C

#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