forked from software-mansion/react-native-screens
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRNSScreenStackHeaderSubview.mm
More file actions
332 lines (274 loc) · 11.4 KB
/
RNSScreenStackHeaderSubview.mm
File metadata and controls
332 lines (274 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
#import "RNSScreenStackHeaderSubview.h"
#import "RNSConvert.h"
#import "RNSDefines.h"
#import "RNSScreenStackHeaderConfig.h"
#import <cxxreact/ReactNativeVersion.h>
#import <react/renderer/components/rnscreens/ComponentDescriptors.h>
#import <react/renderer/components/rnscreens/EventEmitters.h>
#import <react/renderer/components/rnscreens/RCTComponentViewHelpers.h>
#import <React/RCTConversions.h>
#import <React/RCTFabricComponentsPlugins.h>
#import <rnscreens/RNSScreenStackHeaderSubviewComponentDescriptor.h>
namespace react = facebook::react;
@implementation RNSScreenStackHeaderSubview {
react::RNSScreenStackHeaderSubviewShadowNode::ConcreteState::Shared _state;
CGRect _lastScheduledFrame;
// This is a strong reference to UIBarButtonItem which creates a retain cycle.
// The cycle is cleared via `invalidateUIBarButtonItem` method, called by `invalidate` callback.
UIBarButtonItem *_barButtonItem;
BOOL _hidesSharedBackground;
}
#pragma mark - Common
- (nullable RNSScreenStackHeaderConfig *)getHeaderConfig
{
RNSScreenStackHeaderConfig *headerConfig = (RNSScreenStackHeaderConfig *_Nullable)self.reactSuperview;
#ifndef NDEBUG
if (headerConfig != nil && ![headerConfig isKindOfClass:[RNSScreenStackHeaderConfig class]]) {
RCTLogError(@"[RNScreens] Invalid view type, expecting RNSScreenStackHeaderConfig, got: %@", headerConfig);
return nil;
}
#endif
return headerConfig;
}
- (nullable UINavigationBar *)findNavigationBar
{
return [[[[[self getHeaderConfig] screenView] reactViewController] navigationController] navigationBar];
}
// We're forcing the navigation controller's view to re-layout
// see: https://github.com/software-mansion/react-native-screens/pull/2385
- (void)layoutNavigationBar
{
// If we're not attached yet, we should not layout the navigation bar,
// because the layout flow won't reach us & we will clear "isLayoutDirty" flags
// on view above us, causing subsequent layout request to not reach us.
if (self.window == nil) {
return;
}
UIView *toLayoutView = [self findNavigationBar];
// TODO: It is possible, that this call is no longer necessary. Make sure that Test432 keeps working.
[toLayoutView setNeedsLayout];
// TODO: Determine why this must be called & deferring layout to next "update cycle"
// is not sufficient. See Test2552 and Test432.
[toLayoutView layoutIfNeeded];
}
#pragma mark - Fabric specific
- (void)updateShadowStateInContextOfAncestorView:(nullable UIView *)ancestorView withFrame:(CGRect)frame
{
if (ancestorView == nil) {
// We can not compute valid value
return;
}
CGRect convertedFrame = [self convertRect:frame toView:ancestorView];
[self updateShadowStateWithFrame:convertedFrame];
}
- (void)updateShadowStateInContextOfAncestorView:(nullable UIView *)ancestorView
{
[self updateShadowStateInContextOfAncestorView:ancestorView withFrame:self.bounds];
}
- (void)updateShadowStateWithFrame:(CGRect)frame
{
if (_state == nullptr) {
return;
}
if (!CGRectEqualToRect(frame, _lastScheduledFrame)) {
auto newState =
react::RNSScreenStackHeaderSubviewState(RCTSizeFromCGSize(frame.size), RCTPointFromCGPoint(frame.origin));
_state->updateState(
std::move(newState)
#if REACT_NATIVE_VERSION_MINOR >= 82
,
_synchronousShadowStateUpdatesEnabled ? facebook::react::EventQueue::UpdateMode::unstable_Immediate
: facebook::react::EventQueue::UpdateMode::Asynchronous
#endif
);
_lastScheduledFrame = frame;
}
}
- (void)layoutSubviews
{
[super layoutSubviews];
[self updateShadowStateInContextOfAncestorView:[self findNavigationBar]];
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const react::RNSScreenStackHeaderSubviewProps>();
_props = defaultProps;
_lastScheduledFrame = CGRectZero;
}
return self;
}
#pragma mark - RCTComponentViewProtocol
- (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props::Shared const &)oldProps
{
const auto &newHeaderSubviewProps = *std::static_pointer_cast<const react::RNSScreenStackHeaderSubviewProps>(props);
[self setType:[RNSConvert RNSScreenStackHeaderSubviewTypeFromCppEquivalent:newHeaderSubviewProps.type]];
[self setHidesSharedBackground:newHeaderSubviewProps.hidesSharedBackground];
[self setSynchronousShadowStateUpdatesEnabled:newHeaderSubviewProps.synchronousShadowStateUpdatesEnabled];
[super updateProps:props oldProps:oldProps];
}
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
{
return react::concreteComponentDescriptorProvider<react::RNSScreenStackHeaderSubviewComponentDescriptor>();
}
// System layouts the subviews.
RNS_IGNORE_SUPER_CALL_BEGIN
- (void)updateLayoutMetrics:(const react::LayoutMetrics &)layoutMetrics
oldLayoutMetrics:(const react::LayoutMetrics &)oldLayoutMetrics
{
CGRect frame = RCTCGRectFromRect(layoutMetrics.frame);
// CALayer will crash if we pass NaN or Inf values.
// It's unclear how to detect this case on cross-platform manner holistically, so we have to do it on the mounting
// layer as well. NaN/Inf is a kinda valid result of some math operations. Even if we can (and should) detect (and
// report early) incorrect (NaN and Inf) values which come from JavaScript side, we sometimes cannot backtrace the
// sources of a calculation that produced an incorrect/useless result.
if (!std::isfinite(frame.size.width) || !std::isfinite(frame.size.height)) {
RCTLogWarn(
@"-[UIView(ComponentViewProtocol) updateLayoutMetrics:oldLayoutMetrics:]: Received invalid layout metrics (%@) for a view (%@).",
NSStringFromCGRect(frame),
self);
} else {
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
if (self.needsAutoLayout) {
BOOL sizeHasChanged = _layoutMetrics.frame.size != layoutMetrics.frame.size;
_layoutMetrics = layoutMetrics;
if (sizeHasChanged) {
[self invalidateIntrinsicContentSize];
}
} else
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
{
self.bounds = CGRect{CGPointZero, frame.size};
}
[self layoutNavigationBar];
}
}
RNS_IGNORE_SUPER_CALL_END
+ (BOOL)shouldBeRecycled
{
return NO;
}
- (void)updateState:(const facebook::react::State::Shared &)state
oldState:(const facebook::react::State::Shared &)oldState
{
_state = std::static_pointer_cast<const react::RNSScreenStackHeaderSubviewShadowNode::ConcreteState>(state);
}
- (void)invalidate
{
[self invalidateUIBarButtonItem];
}
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
// Starting from iOS 26, to center left and right subviews inside liquid glass backdrop,
// we need to use auto layout. To make Yoga's layout work with auto layout, we pass information
// from Yoga via `intrinsicContentSize`.
- (BOOL)needsAutoLayout
{
BOOL needsAutoLayout = NO;
if (@available(iOS 26.0, *)) {
needsAutoLayout = _type == RNSScreenStackHeaderSubviewTypeLeft || _type == RNSScreenStackHeaderSubviewTypeRight;
}
return needsAutoLayout;
}
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
#pragma mark - UIBarButtonItem specific
- (UIBarButtonItem *)getUIBarButtonItem
{
RCTAssert(
_type == RNSScreenStackHeaderSubviewTypeLeft || _type == RNSScreenStackHeaderSubviewTypeRight,
@"[RNScreens] Unexpected subview type.");
if (_barButtonItem == nil) {
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
if (@available(iOS 26.0, *)) {
// Starting from iOS 26, UIBarButtonItem's customView is streched to have at least 36 width.
// Stretching RNSScreenStackHeaderSubview means that its subviews are aligned to left instead
// of the center. To mitigate this, we add a wrapper view that will center
// RNSScreenStackHeaderSubview inside of itself.
UIView *wrapperView = [UIView new];
wrapperView.translatesAutoresizingMaskIntoConstraints = NO;
self.translatesAutoresizingMaskIntoConstraints = NO;
[wrapperView addSubview:self];
[self.centerXAnchor constraintEqualToAnchor:wrapperView.centerXAnchor].active = YES;
[self.centerYAnchor constraintEqualToAnchor:wrapperView.centerYAnchor].active = YES;
// To prevent UIKit from stretching subviews to all available width, we need to:
// 1. Set width of wrapperView to match RNSScreenStackHeaderSubview BUT when
// RNSScreenStackHeaderSubview's width is smaller that minimal required 36 width, it breaks
// UIKit's constraint. That's why we need to lower the priority of the constraint.
NSLayoutConstraint *widthEqual = [wrapperView.widthAnchor constraintEqualToAnchor:self.widthAnchor];
widthEqual.priority = UILayoutPriorityDefaultHigh;
widthEqual.active = YES;
NSLayoutConstraint *heightEqual = [wrapperView.heightAnchor constraintEqualToAnchor:self.heightAnchor];
heightEqual.priority = UILayoutPriorityDefaultHigh;
heightEqual.active = YES;
// 2. Set content hugging priority for RNSScreenStackHeaderSubview.
[self setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
[self setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
// 3. Set compression resistance to prevent UIKit from shrinking the subview below its intrinsic size.
[self setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
[self setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
_barButtonItem = [[UIBarButtonItem alloc] initWithCustomView:wrapperView];
} else
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
{
_barButtonItem = [[UIBarButtonItem alloc] initWithCustomView:self];
}
[self configureBarButtonItem];
}
return _barButtonItem;
}
- (void)invalidateUIBarButtonItem
{
if (_type != RNSScreenStackHeaderSubviewTypeLeft && _type != RNSScreenStackHeaderSubviewTypeRight) {
return;
}
_barButtonItem = nil;
}
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
- (CGSize)intrinsicContentSize
{
return RCTCGSizeFromSize(_layoutMetrics.frame.size);
}
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
- (void)configureBarButtonItem
{
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
if (@available(iOS 26.0, *)) {
if (_barButtonItem != nil) {
[_barButtonItem setHidesSharedBackground:_hidesSharedBackground];
}
}
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
}
- (void)setHidesSharedBackground:(BOOL)hidesSharedBackground
{
_hidesSharedBackground = hidesSharedBackground;
[self configureBarButtonItem];
}
#pragma mark - Dynamic frameworks support
// Needed because of this: https://github.com/facebook/react-native/pull/37274
#ifdef RCT_DYNAMIC_FRAMEWORKS
+ (void)load
{
[super load];
}
#endif // RCT_DYNAMIC_FRAMEWORKS
@end
@implementation RNSScreenStackHeaderSubviewManager
@end
Class<RCTComponentViewProtocol> RNSScreenStackHeaderSubviewCls(void)
{
return RNSScreenStackHeaderSubview.class;
}
@implementation RCTConvert (RNSScreenStackHeaderSubview)
RCT_ENUM_CONVERTER(
RNSScreenStackHeaderSubviewType,
(@{
@"back" : @(RNSScreenStackHeaderSubviewTypeBackButton),
@"left" : @(RNSScreenStackHeaderSubviewTypeLeft),
@"right" : @(RNSScreenStackHeaderSubviewTypeRight),
@"title" : @(RNSScreenStackHeaderSubviewTypeTitle),
@"center" : @(RNSScreenStackHeaderSubviewTypeCenter),
@"searchBar" : @(RNSScreenStackHeaderSubviewTypeSearchBar),
}),
RNSScreenStackHeaderSubviewTypeTitle,
integerValue)
@end