Skip to content

Commit fdd49f6

Browse files
committed
adding ios 18 zoom functionality to RNN with options
1 parent a25439a commit fdd49f6

7 files changed

Lines changed: 191 additions & 3 deletions

File tree

ios/RNNScreenTransition.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
#import "RNNEnterExitAnimation.h"
33
#import "RNNOptions.h"
44
#import "SharedElementTransitionOptions.h"
5+
#import "Text.h"
6+
7+
@class UIViewController;
58

69
@interface RNNScreenTransition : RNNOptions
710

@@ -10,13 +13,18 @@
1013
@property(nonatomic, strong) ElementTransitionOptions *bottomTabs;
1114
@property(nonatomic, strong) NSArray<ElementTransitionOptions *> *elementTransitions;
1215
@property(nonatomic, strong) NSArray<SharedElementTransitionOptions *> *sharedElementTransitions;
16+
@property(nonatomic, strong) Text *zoomFromId;
17+
@property(nonatomic, strong) Bool *zoomEnabled;
1318

1419
@property(nonatomic, strong) Bool *enable;
1520
@property(nonatomic, strong) Bool *waitForRender;
1621
@property(nonatomic, strong) TimeInterval *duration;
1722

1823
- (BOOL)hasCustomAnimation;
24+
- (BOOL)hasZoomTransition;
1925
- (BOOL)shouldWaitForRender;
2026
- (NSTimeInterval)maxDuration;
27+
- (void)applyZoomToViewController:(UIViewController *)destination
28+
fromSourceViewController:(UIViewController *)source;
2129

2230
@end

ios/RNNScreenTransition.mm

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
#import "RNNScreenTransition.h"
2+
#import "BoolParser.h"
23
#import "OptionsArrayParser.h"
4+
#import "RNNElementFinder.h"
5+
#import "RNNLayoutProtocol.h"
36
#import "RNNUtils.h"
7+
#import "TextParser.h"
8+
#import "UIViewController+LayoutProtocol.h"
9+
#import <UIKit/UIKit.h>
410

511
@implementation RNNScreenTransition
612

@@ -19,6 +25,11 @@ - (instancetype)initWithDict:(NSDictionary *)dict {
1925
self.elementTransitions = [OptionsArrayParser parse:dict
2026
key:@"elementTransitions"
2127
ofClass:ElementTransitionOptions.class];
28+
NSDictionary *zoom = dict[@"zoom"];
29+
if ([zoom isKindOfClass:[NSDictionary class]]) {
30+
self.zoomFromId = [TextParser parse:zoom key:@"fromId"];
31+
self.zoomEnabled = [BoolParser parse:zoom key:@"enabled"];
32+
}
2233

2334
return self;
2435
}
@@ -38,17 +49,58 @@ - (void)mergeOptions:(RNNScreenTransition *)options {
3849
self.sharedElementTransitions = options.sharedElementTransitions;
3950
if (options.elementTransitions)
4051
self.elementTransitions = options.elementTransitions;
52+
if (options.zoomFromId.hasValue)
53+
self.zoomFromId = options.zoomFromId;
54+
if (options.zoomEnabled.hasValue)
55+
self.zoomEnabled = options.zoomEnabled;
4156
}
4257

4358
- (BOOL)hasCustomAnimation {
4459
return (self.topBar.hasAnimation || self.content.hasAnimation || self.bottomTabs.hasAnimation ||
4560
self.sharedElementTransitions || self.elementTransitions);
4661
}
4762

63+
- (BOOL)hasZoomTransition {
64+
if (self.hasCustomAnimation) {
65+
return NO;
66+
}
67+
68+
NSString *fromId = [self.zoomFromId withDefault:@""];
69+
return [self.zoomEnabled withDefault:YES] && fromId.length > 0;
70+
}
71+
4872
- (BOOL)shouldWaitForRender {
4973
return [self.waitForRender withDefault: [RNNUtils getDefaultWaitForRender]] || self.hasCustomAnimation;
5074
}
5175

76+
- (void)applyZoomToViewController:(UIViewController *)destination
77+
fromSourceViewController:(UIViewController *)source {
78+
if (![self hasZoomTransition]) {
79+
return;
80+
}
81+
82+
if (@available(iOS 18.0, *)) {
83+
NSString *fromId = [[self.zoomFromId withDefault:@""] copy];
84+
destination.preferredTransition = [UIViewControllerTransition
85+
zoomWithOptions:nil
86+
sourceViewProvider:^UIView *(UIZoomTransitionSourceViewProviderContext *context) {
87+
UIViewController *sourceVC = context.sourceViewController ?: source;
88+
if (![sourceVC conformsToProtocol:@protocol(RNNLayoutProtocol)]) {
89+
return nil;
90+
}
91+
92+
UIViewController<RNNLayoutProtocol> *rnnSourceVC =
93+
(UIViewController<RNNLayoutProtocol> *)sourceVC;
94+
UIView *reactView = rnnSourceVC.presentedComponentViewController.reactView;
95+
if (reactView == nil) {
96+
return nil;
97+
}
98+
99+
return [RNNElementFinder findElementForId:fromId inView:reactView];
100+
}];
101+
}
102+
}
103+
52104
- (NSTimeInterval)maxDuration {
53105
NSTimeInterval maxDuration = 0;
54106
if ([self.topBar maxDuration] > maxDuration) {

ios/UINavigationController+RNNCommands.mm

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#import "RNNErrorHandler.h"
2+
#import "RNNScreenTransition.h"
23
#import "UINavigationController+RNNCommands.h"
4+
#import "UIViewController+LayoutProtocol.h"
35
#import <React/RCTI18nUtil.h>
46

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

24+
RNNScreenTransition *pushTransition = newTop.resolveOptionsWithDefault.animations.push;
25+
if (animated && [pushTransition hasZoomTransition]) {
26+
[pushTransition applyZoomToViewController:newTop fromSourceViewController:onTopViewController];
27+
}
28+
2229
[self
2330
performBlock:^{
2431
NSLog(@"About to push a controller %@", newTop);

src/commands/OptionsProcessor.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
OptionsSearchBar,
2121
OptionsTopBar,
2222
StackAnimationOptions,
23+
StackPushAnimationOptions,
2324
StatusBarAnimationOptions,
2425
TopBarAnimationOptions,
2526
ViewAnimationOptions,
@@ -390,7 +391,7 @@ export class OptionsProcessor {
390391

391392
private processPush(
392393
key: string,
393-
animation: StackAnimationOptions,
394+
animation: StackPushAnimationOptions,
394395
parentOptions: AnimationOptions
395396
) {
396397
if (key !== 'push') return;

src/interfaces/Options.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -829,6 +829,18 @@ export interface SharedElementTransition {
829829
interpolation?: Interpolation;
830830
}
831831

832+
export interface ZoomTransitionOptions {
833+
/**
834+
* `nativeID` of the view to zoom from when pushing, and zoom back to when popping.
835+
* #### (iOS 18+ specific)
836+
*/
837+
fromId: string;
838+
/**
839+
* @default true
840+
*/
841+
enabled?: boolean;
842+
}
843+
832844
export interface ElementTransition {
833845
id: string;
834846
alpha?: AppearingElementAnimation | DisappearingElementAnimation;
@@ -1537,6 +1549,19 @@ export interface StackAnimationOptions {
15371549
elementTransitions?: ElementTransition[];
15381550
}
15391551

1552+
/**
1553+
* Stack push animations. Extends {@link StackAnimationOptions} with iOS 18+ zoom support.
1554+
*/
1555+
export interface StackPushAnimationOptions extends StackAnimationOptions {
1556+
/**
1557+
* UIKit fluid zoom from a source view (`nativeID` must match `fromId`).
1558+
* Only used for `animations.push` — ignored on `pop`, `setStackRoot`, and Android.
1559+
* Mutually exclusive with `content` / `sharedElementTransitions` on the same push.
1560+
* #### (iOS 18+ specific)
1561+
*/
1562+
zoom?: ZoomTransitionOptions;
1563+
}
1564+
15401565
/**
15411566
* Used for configuring command animations
15421567
*/
@@ -1551,9 +1576,8 @@ export interface AnimationOptions {
15511576
setRoot?: ViewAnimationOptions | EnterExitAnimationOptions;
15521577
/**
15531578
* Configure the animation of the pushed screen
1554-
* #### (Android specific)
15551579
*/
1556-
push?: StackAnimationOptions;
1580+
push?: StackPushAnimationOptions;
15571581
/**
15581582
* Configure what animates when a screen is popped
15591583
*/

website/docs/api/options-animations.mdx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,42 @@ id: options-animations
33
title: Animations
44
sidebar_label: Animations
55
---
6+
7+
Animation options are declared on layout `options.animations`. See the [Animations guide](../docs/style-animations) for examples (stack, modal, shared elements, zoom).
8+
9+
## Stack `push` / `pop`
10+
11+
Stack command animations support `content`, `topBar`, `bottomTabs`, `sharedElementTransitions`, `elementTransitions`, and on iOS 18+ [`zoom`](#zoom-ios-18).
12+
13+
## Zoom (iOS 18+)
14+
15+
Declared under **`animations.push.zoom` only** when pushing onto a stack. Ignored on `animations.pop`, `animations.setStackRoot`, and Android.
16+
17+
```js
18+
animations: {
19+
push: {
20+
zoom: {
21+
fromId: 'my-thumb',
22+
enabled: true, // optional, default true
23+
},
24+
},
25+
}
26+
```
27+
28+
| Property | Type | Required | Platform | Description |
29+
| -------- | ---- | -------- | -------- | ----------- |
30+
| `fromId` | `string` | Yes | iOS 18+ | Matches `nativeID` on the source view in the screen being pushed from. |
31+
| `enabled` | `boolean` | No | iOS 18+ | Default `true`. |
32+
33+
See [Zoom transition (iOS 18+)](../docs/style-animations#zoom-transition-ios-18) for usage and behavior.
34+
35+
## Shared element transitions
36+
37+
Array under `animations.push.sharedElementTransitions` / `animations.pop.sharedElementTransitions`. Each item:
38+
39+
| Property | Type | Description |
40+
| -------- | ---- | ----------- |
41+
| `fromId` | `string` | `nativeID` on the source screen |
42+
| `toId` | `string` | `nativeID` on the destination screen |
43+
| `duration` | `number` | Duration in ms |
44+
| `interpolation` | `object` | Easing — see [Animations guide](../docs/style-animations#step-3---declare-the-shared-element-animation-when-pushing-the-screen) |

website/docs/docs/style-animations.mdx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,63 @@ options: {
113113
</TabItem>
114114
</Tabs>
115115

116+
### Zoom transition (iOS 18+)
117+
118+
Use UIKit's system **fluid zoom transition** when pushing onto a stack: the tapped view morphs into the next screen. The transition is interactive — users can drag to slow, reverse, or dismiss.
119+
120+
:::info Platform support
121+
Configure under **`animations.push` only** (not `setStackRoot` or `pop`). Available on **iOS 18 and later**. Android ignores this option. Reverse zoom on pop is automatic when the detail screen is popped.
122+
:::
123+
124+
This uses `UIViewController.preferredTransition` under the hood. It is separate from [shared element transitions](#shared-element-transitions): you do not declare `sharedElementTransitions` or `content` animations for zoom. If those custom animations are set on the same push, they take precedence and zoom is not applied.
125+
126+
#### Step 1 — Set `nativeID` on the source view
127+
128+
Mark the view that should expand (thumbnail, card, hero image, etc.):
129+
130+
```jsx
131+
<View nativeID={`product-thumb-${item.id}`} style={styles.thumbnail}>
132+
<Image source={item.image} style={styles.image} />
133+
</View>
134+
```
135+
136+
#### Step 2 — Pass the same id in push options
137+
138+
```jsx
139+
const fromId = `product-thumb-${item.id}`;
140+
141+
Navigation.push(componentId, {
142+
component: {
143+
name: 'ProductDetail',
144+
passProps: { item },
145+
options: {
146+
animations: {
147+
push: {
148+
zoom: {
149+
fromId,
150+
},
151+
},
152+
},
153+
},
154+
},
155+
});
156+
```
157+
158+
On pop, UIKit runs the reverse zoom automatically using the same `fromId` and `nativeID`. You do not need a separate `animations.pop` block for zoom.
159+
160+
#### Options
161+
162+
| Property | Type | Required | Description |
163+
| -------- | ---- | -------- | ----------- |
164+
| `fromId` | `string` | Yes | Must match the `nativeID` of the source view on the screen below. |
165+
| `enabled` | `boolean` | No | Default `true`. Set `false` to skip zoom while keeping the option object. |
166+
167+
#### Notes
168+
169+
- The source view must be mounted and visible when the push runs. If RNN cannot resolve `fromId`, the push falls back to the default slide animation.
170+
- `fromId` is resolved with the same mechanism as shared element `fromId` / `toId` (search in the source screen's React view hierarchy).
171+
- Zoom is intended for list → detail flows. For fully custom cross-screen animations, use [shared element transitions](#shared-element-transitions).
172+
116173
### Modal animations
117174

118175
Modal animations are declared similarly to stack animations, only this time we animate the entire view and not only part of the UI (content).

0 commit comments

Comments
 (0)