Skip to content

Commit e761eb7

Browse files
Add scrollEdgeEffect option to control iOS 26 scroll edge effects (#8281)
Co-authored-by: mark.dev <50657916+markdevocht@users.noreply.github.com>
1 parent 464e137 commit e761eb7

6 files changed

Lines changed: 211 additions & 0 deletions

File tree

ios/RNNComponentPresenter.mm

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,83 @@
11
#import "RNNComponentPresenter.h"
22
#import "RNNComponentViewController.h"
3+
#import "RNNScrollEdgeEffectOptions.h"
34
#import "TopBarTitlePresenter.h"
45
#import "UITabBarController+RNNOptions.h"
56
#import "UIViewController+RNNOptions.h"
67

8+
static NSNumber *RNNScrollEdgeEffectStyleFromString(NSString *value) {
9+
if ([value isEqualToString:@"hard"])
10+
return @(2);
11+
if ([value isEqualToString:@"soft"])
12+
return @(1);
13+
return @(0); // automatic
14+
}
15+
16+
static RNNScrollEdgeOptions *RNNScrollEdgeOptionsForKey(RNNScrollEdgeEffectOptions *options,
17+
NSString *edgeKey) {
18+
if ([edgeKey isEqualToString:@"topEdgeEffect"])
19+
return options.top;
20+
if ([edgeKey isEqualToString:@"bottomEdgeEffect"])
21+
return options.bottom;
22+
if ([edgeKey isEqualToString:@"leftEdgeEffect"])
23+
return options.left;
24+
if ([edgeKey isEqualToString:@"rightEdgeEffect"])
25+
return options.right;
26+
return nil;
27+
}
28+
29+
static BOOL RNNScrollEdgeEffectHasAnyValue(RNNScrollEdgeEffectOptions *options) {
30+
if (!options)
31+
return NO;
32+
if (options.hidden.hasValue || options.style.hasValue)
33+
return YES;
34+
NSArray<RNNScrollEdgeOptions *> *edges =
35+
@[ options.top ?: [RNNScrollEdgeOptions new], options.bottom ?: [RNNScrollEdgeOptions new],
36+
options.left ?: [RNNScrollEdgeOptions new], options.right ?: [RNNScrollEdgeOptions new] ];
37+
for (RNNScrollEdgeOptions *edge in edges) {
38+
if (edge.hidden.hasValue || edge.style.hasValue)
39+
return YES;
40+
}
41+
return NO;
42+
}
43+
44+
static void RNNApplyScrollEdgeEffectToScrollView(UIScrollView *scrollView,
45+
RNNScrollEdgeEffectOptions *options) {
46+
if (![scrollView respondsToSelector:NSSelectorFromString(@"topEdgeEffect")])
47+
return;
48+
NSArray<NSString *> *edgeKeys =
49+
@[ @"topEdgeEffect", @"bottomEdgeEffect", @"leftEdgeEffect", @"rightEdgeEffect" ];
50+
for (NSString *key in edgeKeys) {
51+
id effect = [scrollView valueForKey:key];
52+
if (!effect)
53+
continue;
54+
RNNScrollEdgeOptions *perEdge = RNNScrollEdgeOptionsForKey(options, key);
55+
56+
Bool *hidden = (perEdge.hidden.hasValue) ? perEdge.hidden : options.hidden;
57+
Text *style = (perEdge.style.hasValue) ? perEdge.style : options.style;
58+
59+
if (hidden.hasValue &&
60+
[effect respondsToSelector:NSSelectorFromString(@"setHidden:")]) {
61+
[effect setValue:@([hidden withDefault:NO]) forKey:@"hidden"];
62+
}
63+
if (style.hasValue &&
64+
[effect respondsToSelector:NSSelectorFromString(@"setStyle:")]) {
65+
[effect setValue:RNNScrollEdgeEffectStyleFromString(style.get) forKey:@"style"];
66+
}
67+
}
68+
}
69+
70+
static void RNNApplyScrollEdgeEffectToView(UIView *view, RNNScrollEdgeEffectOptions *options) {
71+
if (!RNNScrollEdgeEffectHasAnyValue(options))
72+
return;
73+
if ([view isKindOfClass:[UIScrollView class]]) {
74+
RNNApplyScrollEdgeEffectToScrollView((UIScrollView *)view, options);
75+
}
76+
for (UIView *subview in view.subviews) {
77+
RNNApplyScrollEdgeEffectToView(subview, options);
78+
}
79+
}
80+
781
@implementation RNNComponentPresenter {
882
TopBarTitlePresenter *_topBarTitlePresenter;
983
RNNButtonsPresenter *_buttonsPresenter;
@@ -34,6 +108,11 @@ - (void)componentWillAppear {
34108
- (void)componentDidAppear {
35109
[_topBarTitlePresenter componentDidAppear];
36110
[_buttonsPresenter componentDidAppear];
111+
112+
RNNComponentViewController *viewController = self.boundViewController;
113+
RNNNavigationOptions *withDefault =
114+
[viewController.options withDefault:[self defaultOptions]];
115+
RNNApplyScrollEdgeEffectToView(viewController.view, withDefault.scrollEdgeEffect);
37116
}
38117

39118
- (void)componentDidDisappear {
@@ -102,6 +181,8 @@ - (void)applyOptions:(RNNNavigationOptions *)options {
102181
defaultDisabledColor:withDefault.topBar.rightButtonDisabledColor
103182
animated:[withDefault.topBar.animateRightButtons withDefault:NO]];
104183
}
184+
185+
RNNApplyScrollEdgeEffectToView(viewController.view, withDefault.scrollEdgeEffect);
105186
}
106187

107188
- (void)applyOptionsOnInit:(RNNNavigationOptions *)options {
@@ -236,6 +317,10 @@ - (void)mergeOptions:(RNNNavigationOptions *)mergeOptions
236317
}
237318

238319
[_topBarTitlePresenter mergeOptions:mergeOptions.topBar resolvedOptions:withDefault.topBar];
320+
321+
if (RNNScrollEdgeEffectHasAnyValue(mergeOptions.scrollEdgeEffect)) {
322+
RNNApplyScrollEdgeEffectToView(viewController.view, mergeOptions.scrollEdgeEffect);
323+
}
239324
}
240325

241326
- (void)renderComponents:(RNNNavigationOptions *)options

ios/RNNNavigationOptions.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#import "RNNModalOptions.h"
77
#import "RNNOverlayOptions.h"
88
#import "RNNPreviewOptions.h"
9+
#import "RNNScrollEdgeEffectOptions.h"
910
#import "RNNSharedElementAnimationOptions.h"
1011
#import "RNNSideMenuOptions.h"
1112
#import "RNNSplitViewOptions.h"
@@ -34,6 +35,7 @@ extern const NSInteger BLUR_TOPBAR_TAG;
3435
@property(nonatomic, strong) RNNModalOptions *modal;
3536
@property(nonatomic, strong) DeprecationOptions *deprecations;
3637
@property(nonatomic, strong) WindowOptions *window;
38+
@property(nonatomic, strong) RNNScrollEdgeEffectOptions *scrollEdgeEffect;
3739

3840
@property(nonatomic, strong) Bool *popGesture;
3941
@property(nonatomic, strong) Bool *navigationButtonEventOnSwipeBack;

ios/RNNNavigationOptions.mm

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ - (instancetype)initWithDict:(NSDictionary *)dict {
3939
self.modal = [[RNNModalOptions alloc] initWithDict:dict[@"modal"]];
4040
self.deprecations = [[DeprecationOptions alloc] initWithDict:dict[@"deprecations"]];
4141
self.window = [[WindowOptions alloc] initWithDict:dict[@"window"]];
42+
self.scrollEdgeEffect =
43+
[[RNNScrollEdgeEffectOptions alloc] initWithDict:dict[@"scrollEdgeEffect"]];
4244

4345
self.popGesture = [[Bool alloc] initWithValue:dict[@"popGesture"]];
4446
self.navigationButtonEventOnSwipeBack = [[Bool alloc] initWithValue:dict[@"navigationButtonEventOnSwipeBack"]];
@@ -71,6 +73,7 @@ - (RNNNavigationOptions *)mergeOptions:(RNNNavigationOptions *)options {
7173
[result.modal mergeOptions:options.modal];
7274
[result.deprecations mergeOptions:options.deprecations];
7375
[result.window mergeOptions:options.window];
76+
[result.scrollEdgeEffect mergeOptions:options.scrollEdgeEffect];
7477

7578
if (options.popGesture.hasValue)
7679
result.popGesture = options.popGesture;
@@ -105,6 +108,7 @@ - (RNNNavigationOptions *)copy {
105108
[newOptions.modal mergeOptions:self.modal];
106109
[newOptions.deprecations mergeOptions:self.deprecations];
107110
[newOptions.window mergeOptions:self.window];
111+
[newOptions.scrollEdgeEffect mergeOptions:self.scrollEdgeEffect];
108112

109113
newOptions.popGesture = self.popGesture;
110114
newOptions.navigationButtonEventOnSwipeBack = self.navigationButtonEventOnSwipeBack;

ios/RNNScrollEdgeEffectOptions.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#import "RNNOptions.h"
2+
3+
@interface RNNScrollEdgeOptions : RNNOptions
4+
5+
@property(nonatomic, strong) Bool *hidden;
6+
@property(nonatomic, strong) Text *style;
7+
8+
@end
9+
10+
@interface RNNScrollEdgeEffectOptions : RNNOptions
11+
12+
@property(nonatomic, strong) Bool *hidden;
13+
@property(nonatomic, strong) Text *style;
14+
15+
@property(nonatomic, strong) RNNScrollEdgeOptions *top;
16+
@property(nonatomic, strong) RNNScrollEdgeOptions *bottom;
17+
@property(nonatomic, strong) RNNScrollEdgeOptions *left;
18+
@property(nonatomic, strong) RNNScrollEdgeOptions *right;
19+
20+
@end

ios/RNNScrollEdgeEffectOptions.mm

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#import "RNNScrollEdgeEffectOptions.h"
2+
3+
@implementation RNNScrollEdgeOptions
4+
5+
- (instancetype)initWithDict:(NSDictionary *)dict {
6+
self = [super initWithDict:dict];
7+
self.hidden = [BoolParser parse:dict key:@"hidden"];
8+
self.style = [TextParser parse:dict key:@"style"];
9+
return self;
10+
}
11+
12+
- (void)mergeOptions:(RNNScrollEdgeOptions *)options {
13+
if (options.hidden.hasValue)
14+
self.hidden = options.hidden;
15+
if (options.style.hasValue)
16+
self.style = options.style;
17+
}
18+
19+
@end
20+
21+
@implementation RNNScrollEdgeEffectOptions
22+
23+
- (instancetype)initWithDict:(NSDictionary *)dict {
24+
self = [super initWithDict:dict];
25+
self.hidden = [BoolParser parse:dict key:@"hidden"];
26+
self.style = [TextParser parse:dict key:@"style"];
27+
self.top = [[RNNScrollEdgeOptions alloc] initWithDict:dict[@"top"]];
28+
self.bottom = [[RNNScrollEdgeOptions alloc] initWithDict:dict[@"bottom"]];
29+
self.left = [[RNNScrollEdgeOptions alloc] initWithDict:dict[@"left"]];
30+
self.right = [[RNNScrollEdgeOptions alloc] initWithDict:dict[@"right"]];
31+
return self;
32+
}
33+
34+
- (void)mergeOptions:(RNNScrollEdgeEffectOptions *)options {
35+
if (options.hidden.hasValue)
36+
self.hidden = options.hidden;
37+
if (options.style.hasValue)
38+
self.style = options.style;
39+
[self.top mergeOptions:options.top];
40+
[self.bottom mergeOptions:options.bottom];
41+
[self.left mergeOptions:options.left];
42+
[self.right mergeOptions:options.right];
43+
}
44+
45+
@end

src/interfaces/Options.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1721,10 +1721,65 @@ setRoot: {
17211721
* #### (iOS specific)
17221722
*/
17231723
window?: WindowOptions;
1724+
/**
1725+
* Show / hide or change the style of the iOS 26 scroll edge effect on every
1726+
* UIScrollView contained in the screen.
1727+
* #### (iOS 26+ specific)
1728+
*/
1729+
scrollEdgeEffect?: OptionsScrollEdgeEffect;
17241730
/**
17251731
* Enable or disable automatically blurring focused input, dismissing keyboard on unmount
17261732
* #### (Android specific)
17271733
* @default false
17281734
*/
17291735
blurOnUnmount?: boolean;
17301736
}
1737+
1738+
export interface OptionsScrollEdge {
1739+
/**
1740+
* Hide the scroll edge effect on this edge.
1741+
* #### (iOS 26+ specific)
1742+
*/
1743+
hidden?: boolean;
1744+
/**
1745+
* Style of the scroll edge effect on this edge.
1746+
* #### (iOS 26+ specific)
1747+
* @default 'automatic'
1748+
*/
1749+
style?: 'automatic' | 'soft' | 'hard';
1750+
}
1751+
1752+
export interface OptionsScrollEdgeEffect {
1753+
/**
1754+
* Hide the scroll edge effect on all four edges of contained scroll views.
1755+
* Per-edge values (top / bottom / left / right) take precedence over this.
1756+
* #### (iOS 26+ specific)
1757+
*/
1758+
hidden?: boolean;
1759+
/**
1760+
* Style of the scroll edge effect on all four edges. Per-edge values take precedence.
1761+
* #### (iOS 26+ specific)
1762+
* @default 'automatic'
1763+
*/
1764+
style?: 'automatic' | 'soft' | 'hard';
1765+
/**
1766+
* Per-edge override for the top edge effect. Falls back to the global hidden / style.
1767+
* #### (iOS 26+ specific)
1768+
*/
1769+
top?: OptionsScrollEdge;
1770+
/**
1771+
* Per-edge override for the bottom edge effect. Falls back to the global hidden / style.
1772+
* #### (iOS 26+ specific)
1773+
*/
1774+
bottom?: OptionsScrollEdge;
1775+
/**
1776+
* Per-edge override for the left edge effect. Falls back to the global hidden / style.
1777+
* #### (iOS 26+ specific)
1778+
*/
1779+
left?: OptionsScrollEdge;
1780+
/**
1781+
* Per-edge override for the right edge effect. Falls back to the global hidden / style.
1782+
* #### (iOS 26+ specific)
1783+
*/
1784+
right?: OptionsScrollEdge;
1785+
}

0 commit comments

Comments
 (0)