Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions ios/RNNScreenTransition.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
#import "RNNEnterExitAnimation.h"
#import "RNNOptions.h"
#import "SharedElementTransitionOptions.h"
#import "Text.h"

@class UIViewController;

@interface RNNScreenTransition : RNNOptions

Expand All @@ -10,13 +13,18 @@
@property(nonatomic, strong) ElementTransitionOptions *bottomTabs;
@property(nonatomic, strong) NSArray<ElementTransitionOptions *> *elementTransitions;
@property(nonatomic, strong) NSArray<SharedElementTransitionOptions *> *sharedElementTransitions;
@property(nonatomic, strong) Text *zoomFromId;
@property(nonatomic, strong) Bool *zoomEnabled;

@property(nonatomic, strong) Bool *enable;
@property(nonatomic, strong) Bool *waitForRender;
@property(nonatomic, strong) TimeInterval *duration;

- (BOOL)hasCustomAnimation;
- (BOOL)hasZoomTransition;
- (BOOL)shouldWaitForRender;
- (NSTimeInterval)maxDuration;
- (void)applyZoomToViewController:(UIViewController *)destination
fromSourceViewController:(UIViewController *)source;

@end
52 changes: 52 additions & 0 deletions ios/RNNScreenTransition.mm
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
#import "RNNScreenTransition.h"
#import "BoolParser.h"
#import "OptionsArrayParser.h"
#import "RNNElementFinder.h"
#import "RNNLayoutProtocol.h"
#import "RNNUtils.h"
#import "TextParser.h"
#import "UIViewController+LayoutProtocol.h"
#import <UIKit/UIKit.h>

@implementation RNNScreenTransition

Expand All @@ -19,6 +25,11 @@ - (instancetype)initWithDict:(NSDictionary *)dict {
self.elementTransitions = [OptionsArrayParser parse:dict
key:@"elementTransitions"
ofClass:ElementTransitionOptions.class];
NSDictionary *zoom = dict[@"zoom"];
if ([zoom isKindOfClass:[NSDictionary class]]) {
self.zoomFromId = [TextParser parse:zoom key:@"fromId"];
self.zoomEnabled = [BoolParser parse:zoom key:@"enabled"];
}

return self;
}
Expand All @@ -38,17 +49,58 @@ - (void)mergeOptions:(RNNScreenTransition *)options {
self.sharedElementTransitions = options.sharedElementTransitions;
if (options.elementTransitions)
self.elementTransitions = options.elementTransitions;
if (options.zoomFromId.hasValue)
self.zoomFromId = options.zoomFromId;
if (options.zoomEnabled.hasValue)
self.zoomEnabled = options.zoomEnabled;
}

- (BOOL)hasCustomAnimation {
return (self.topBar.hasAnimation || self.content.hasAnimation || self.bottomTabs.hasAnimation ||
self.sharedElementTransitions || self.elementTransitions);
}

- (BOOL)hasZoomTransition {
if (self.hasCustomAnimation) {
return NO;
}

NSString *fromId = [self.zoomFromId withDefault:@""];
return [self.zoomEnabled withDefault:YES] && fromId.length > 0;
}

- (BOOL)shouldWaitForRender {
return [self.waitForRender withDefault: [RNNUtils getDefaultWaitForRender]] || self.hasCustomAnimation;
}

- (void)applyZoomToViewController:(UIViewController *)destination
fromSourceViewController:(UIViewController *)source {
if (![self hasZoomTransition]) {
return;
}

if (@available(iOS 18.0, *)) {
NSString *fromId = [[self.zoomFromId withDefault:@""] copy];
destination.preferredTransition = [UIViewControllerTransition
zoomWithOptions:nil
sourceViewProvider:^UIView *(UIZoomTransitionSourceViewProviderContext *context) {
UIViewController *sourceVC = context.sourceViewController ?: source;
if (![sourceVC conformsToProtocol:@protocol(RNNLayoutProtocol)]) {
return nil;
}

UIViewController<RNNLayoutProtocol> *rnnSourceVC =
(UIViewController<RNNLayoutProtocol> *)sourceVC;
UIView *reactView = rnnSourceVC.presentedComponentViewController.reactView;
if (reactView == nil) {
return nil;
}

return [RNNElementFinder findElementForId:fromId inView:reactView];
}];
}
}

- (NSTimeInterval)maxDuration {
NSTimeInterval maxDuration = 0;
if ([self.topBar maxDuration] > maxDuration) {
Expand Down
7 changes: 7 additions & 0 deletions ios/UINavigationController+RNNCommands.mm
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#import "RNNErrorHandler.h"
#import "RNNScreenTransition.h"
#import "UINavigationController+RNNCommands.h"
#import "UIViewController+LayoutProtocol.h"
#import <React/RCTI18nUtil.h>

typedef void (^RNNAnimationBlock)(void);
Expand All @@ -19,6 +21,11 @@ - (void)push:(UIViewController *)newTop
self.navigationBar.semanticContentAttribute = UISemanticContentAttributeForceLeftToRight;
}

RNNScreenTransition *pushTransition = newTop.resolveOptionsWithDefault.animations.push;
if (animated && [pushTransition hasZoomTransition]) {
[pushTransition applyZoomToViewController:newTop fromSourceViewController:onTopViewController];
}

[self
performBlock:^{
NSLog(@"About to push a controller %@", newTop);
Expand Down
89 changes: 89 additions & 0 deletions playground/ios/NavigationTests/RNNScreenTransitionTest.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#import "RNNScreenTransition.h"
#import "RNNUtils.h"
#import <XCTest/XCTest.h>

@interface RNNScreenTransitionTest : XCTestCase

@end

@implementation RNNScreenTransitionTest

- (void)testParsesZoomFromId {
RNNScreenTransition *transition =
[[RNNScreenTransition alloc] initWithDict:@{@"zoom" : @{@"fromId" : @"thumb-1"}}];

XCTAssertEqualObjects(transition.zoomFromId.get, @"thumb-1");
XCTAssertTrue(transition.hasZoomTransition);
}

- (void)testZoomEnabledDefaultsToTrue {
RNNScreenTransition *transition =
[[RNNScreenTransition alloc] initWithDict:@{@"zoom" : @{@"fromId" : @"thumb-1"}}];

XCTAssertTrue(transition.hasZoomTransition);
}

- (void)testZoomEnabledFalseDisablesZoomTransition {
RNNScreenTransition *transition = [[RNNScreenTransition alloc]
initWithDict:@{@"zoom" : @{@"fromId" : @"thumb-1", @"enabled" : @NO}}];

XCTAssertFalse(transition.hasZoomTransition);
}

- (void)testZoomTransitionRequiresNonEmptyFromId {
RNNScreenTransition *missingFromId =
[[RNNScreenTransition alloc] initWithDict:@{@"zoom" : @{}}];
RNNScreenTransition *emptyFromId =
[[RNNScreenTransition alloc] initWithDict:@{@"zoom" : @{@"fromId" : @""}}];

XCTAssertFalse(missingFromId.hasZoomTransition);
XCTAssertFalse(emptyFromId.hasZoomTransition);
}

- (void)testCustomContentAnimationTakesPrecedenceOverZoomTransition {
RNNScreenTransition *transition = [[RNNScreenTransition alloc] initWithDict:@{
@"zoom" : @{@"fromId" : @"thumb-1"},
@"content" : @{@"enter" : @{@"alpha" : @{@"from" : @0, @"to" : @1}}}
}];

XCTAssertTrue(transition.hasCustomAnimation);
XCTAssertFalse(transition.hasZoomTransition);
}

- (void)testSharedElementTransitionTakesPrecedenceOverZoomTransition {
RNNScreenTransition *transition = [[RNNScreenTransition alloc] initWithDict:@{
@"zoom" : @{@"fromId" : @"thumb-1"},
@"sharedElementTransitions" : @[ @{@"fromId" : @"image-1", @"toId" : @"image-2"} ]
}];

XCTAssertTrue(transition.hasCustomAnimation);
XCTAssertFalse(transition.hasZoomTransition);
}

- (void)testMergeOptionsUpdatesZoomFromIdAndEnabled {
RNNScreenTransition *transition =
[[RNNScreenTransition alloc] initWithDict:@{@"zoom" : @{@"fromId" : @"thumb-1"}}];
RNNScreenTransition *updatedFromId =
[[RNNScreenTransition alloc] initWithDict:@{@"zoom" : @{@"fromId" : @"thumb-2"}}];
RNNScreenTransition *disabled =
[[RNNScreenTransition alloc] initWithDict:@{@"zoom" : @{@"enabled" : @NO}}];

[transition mergeOptions:updatedFromId];
XCTAssertEqualObjects(transition.zoomFromId.get, @"thumb-2");
XCTAssertTrue(transition.hasZoomTransition);

[transition mergeOptions:disabled];
XCTAssertFalse(transition.hasZoomTransition);
}

- (void)testShouldWaitForRenderUsesDefaultForZoomOnlyAndCustomAnimationsWait {
RNNScreenTransition *zoomOnly =
[[RNNScreenTransition alloc] initWithDict:@{@"zoom" : @{@"fromId" : @"thumb-1"}}];
RNNScreenTransition *customAnimation = [[RNNScreenTransition alloc]
initWithDict:@{@"content" : @{@"enter" : @{@"alpha" : @{@"from" : @0, @"to" : @1}}}}];

XCTAssertEqual(zoomOnly.shouldWaitForRender, [RNNUtils getDefaultWaitForRender]);
XCTAssertTrue(customAnimation.shouldWaitForRender);
}

@end
4 changes: 4 additions & 0 deletions playground/ios/playground.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
50C9A8D4240FB9D000BD699F /* RNNComponentViewController+Utils.mm in Sources */ = {isa = PBXBuildFile; fileRef = 50C9A8D3240FB9D000BD699F /* RNNComponentViewController+Utils.mm */; };
50CF233D240695B10098042D /* RNNBottomTabsController+Helpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 50CF233C240695B10098042D /* RNNBottomTabsController+Helpers.mm */; };
50FDEFBC258F5C5D008C9C3C /* RNNSearchBarOptionsTest.mm in Sources */ = {isa = PBXBuildFile; fileRef = 50FDEFBB258F5C5D008C9C3C /* RNNSearchBarOptionsTest.mm */; };
57C883092EB720D100830800 /* RNNScreenTransitionTest.mm in Sources */ = {isa = PBXBuildFile; fileRef = 57C883082EB720D100830800 /* RNNScreenTransitionTest.mm */; };
6B102251DCC578519C2DC6A4 /* libPods-NavigationIOS12Tests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C10F72071A488F801E1F1116 /* libPods-NavigationIOS12Tests.a */; };
8EB60A6CB93C527CC6A870A2 /* libPods-SnapshotTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E8B4CFA18A5ACE953124E129 /* libPods-SnapshotTests.a */; };
9F9A3A9625260DA900AAAF37 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9F9A3A9525260DA900AAAF37 /* LaunchScreen.storyboard */; };
Expand Down Expand Up @@ -166,6 +167,7 @@
50CF233C240695B10098042D /* RNNBottomTabsController+Helpers.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = "RNNBottomTabsController+Helpers.mm"; sourceTree = "<group>"; };
50E4888A2427DA4800B11A8E /* StackOptionsTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = StackOptionsTest.mm; sourceTree = "<group>"; };
50FDEFBB258F5C5D008C9C3C /* RNNSearchBarOptionsTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RNNSearchBarOptionsTest.mm; sourceTree = "<group>"; };
57C883082EB720D100830800 /* RNNScreenTransitionTest.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RNNScreenTransitionTest.mm; sourceTree = "<group>"; };
60BCFCC02B7F812F00FCDB38 /* libLLVM.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libLLVM.dylib; path = usr/lib/libLLVM.dylib; sourceTree = SDKROOT; };
60BCFCCA2B7F817400FCDB38 /* libReactNativeNavigation.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libReactNativeNavigation.a; sourceTree = BUILT_PRODUCTS_DIR; };
7F8E255E2E08F6ECE7DF6FE3 /* Pods-playground.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-playground.release.xcconfig"; path = "Target Support Files/Pods-playground/Pods-playground.release.xcconfig"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -429,6 +431,7 @@
5022EDCB240551EE00852BA6 /* RNNBottomTabsAppearancePresenterTest.mm */,
E58D26342385888B003F36BA /* RNNTestRootViewCreator.h */,
E58D263D2385888C003F36BA /* RNNTestRootViewCreator.mm */,
57C883082EB720D100830800 /* RNNScreenTransitionTest.mm */,
E58D26382385888B003F36BA /* RNNTransitionStateHolderTest.mm */,
E58D263B2385888C003F36BA /* UITabBarController+RNNOptionsTest.mm */,
E58D26252385888B003F36BA /* UIViewController+LayoutProtocolTest.mm */,
Expand Down Expand Up @@ -979,6 +982,7 @@
E58D26582385888C003F36BA /* UITabBarController+RNNOptionsTest.mm in Sources */,
E58D265A2385888C003F36BA /* RNNTestRootViewCreator.mm in Sources */,
E58D26492385888C003F36BA /* RNNFontAttributesCreatorTest.mm in Sources */,
57C883092EB720D100830800 /* RNNScreenTransitionTest.mm in Sources */,
E58D26552385888C003F36BA /* RNNTransitionStateHolderTest.mm in Sources */,
E58D26462385888C003F36BA /* UIViewController+LayoutProtocolTest.mm in Sources */,
E58D26512385888C003F36BA /* RNNExternalComponentStoreTest.mm in Sources */,
Expand Down
110 changes: 110 additions & 0 deletions src/commands/OptionsProcessor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,9 @@ describe('navigation options', () => {
waitForRender: true,
sharedElementTransitions: [],
elementTransitions: [],
zoom: {
fromId: 'thumb-1',
},
},
},
};
Expand All @@ -899,9 +902,116 @@ describe('navigation options', () => {
waitForRender: true,
sharedElementTransitions: [],
elementTransitions: [],
zoom: {
fromId: 'thumb-1',
},
});
});
});

it('preserves push zoom options', () => {
const options: Options = {
animations: {
push: {
zoom: {
fromId: 'thumb-1',
},
},
},
};

uut.processOptions(CommandName.SetRoot, options);

expect(options.animations!!.push).toStrictEqual({
zoom: {
fromId: 'thumb-1',
},
});
});

it('preserves disabled push zoom options', () => {
const options: Options = {
animations: {
push: {
zoom: {
fromId: 'thumb-1',
enabled: false,
},
},
},
};

uut.processOptions(CommandName.SetRoot, options);

expect(options.animations!!.push).toStrictEqual({
zoom: {
fromId: 'thumb-1',
enabled: false,
},
});
});

it('does not treat zoom as a legacy push animation shape', () => {
const options: Options = {
animations: {
push: {
zoom: {
fromId: 'thumb-1',
},
content: {
alpha: {
from: 0,
to: 1,
},
},
topBar: {
alpha: {
from: 0,
to: 1,
},
},
bottomTabs: {
alpha: {
from: 0,
to: 1,
},
},
},
},
};

uut.processOptions(CommandName.SetRoot, options);

expect(options.animations!!.push).toStrictEqual({
zoom: {
fromId: 'thumb-1',
},
content: {
enter: {
alpha: {
from: 0,
to: 1,
},
},
},
topBar: {
enter: {
alpha: {
from: 0,
to: 1,
},
},
},
bottomTabs: {
enter: {
alpha: {
from: 0,
to: 1,
},
},
},
});
});
});

describe('pop', () => {
Expand Down
3 changes: 2 additions & 1 deletion src/commands/OptionsProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
OptionsSearchBar,
OptionsTopBar,
StackAnimationOptions,
StackPushAnimationOptions,
StatusBarAnimationOptions,
TopBarAnimationOptions,
ViewAnimationOptions,
Expand Down Expand Up @@ -390,7 +391,7 @@ export class OptionsProcessor {

private processPush(
key: string,
animation: StackAnimationOptions,
animation: StackPushAnimationOptions,
parentOptions: AnimationOptions
) {
if (key !== 'push') return;
Expand Down
Loading