Skip to content

Commit cf6c03b

Browse files
committed
fix: preserve UIKit tab bar hit testing
1 parent dd50412 commit cf6c03b

4 files changed

Lines changed: 172 additions & 35 deletions

File tree

packages/react-native/ios/Fabric/NativeScriptUIViewComponentView.mm

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,42 @@
88

99
using namespace facebook::react;
1010

11+
static BOOL NativeScriptFabricViewIsDescendantOfView(UIView* view, UIView* ancestor) {
12+
UIView* current = view;
13+
while (current != nil) {
14+
if (current == ancestor) {
15+
return YES;
16+
}
17+
current = current.superview;
18+
}
19+
return NO;
20+
}
21+
22+
static UITabBar* NativeScriptFabricVisibleTabBarAtPoint(
23+
UIView* root,
24+
UIWindow* window,
25+
CGPoint windowPoint) {
26+
if (root.hidden || root.alpha <= 0.01 || !root.userInteractionEnabled) {
27+
return nil;
28+
}
29+
30+
if ([root isKindOfClass:UITabBar.class]) {
31+
CGPoint localPoint = [root convertPoint:windowPoint fromView:window];
32+
if ([root pointInside:localPoint withEvent:nil]) {
33+
return static_cast<UITabBar*>(root);
34+
}
35+
}
36+
37+
for (UIView* subview in [root.subviews reverseObjectEnumerator]) {
38+
UITabBar* tabBar = NativeScriptFabricVisibleTabBarAtPoint(subview, window, windowPoint);
39+
if (tabBar != nil) {
40+
return tabBar;
41+
}
42+
}
43+
44+
return nil;
45+
}
46+
1147
@implementation NativeScriptUIViewComponentView {
1248
NativeScriptUIView* _containerView;
1349
NSString* _debugName;
@@ -56,6 +92,31 @@ - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol>*)childCompon
5692
[childComponentView removeFromSuperview];
5793
}
5894

95+
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event {
96+
UIView* hitView = [super hitTest:point withEvent:event];
97+
if (hitView == nil || self.window == nil) {
98+
return hitView;
99+
}
100+
101+
CGPoint windowPoint = [self convertPoint:point toView:self.window];
102+
UITabBar* tabBar =
103+
NativeScriptFabricVisibleTabBarAtPoint(self.window, self.window, windowPoint);
104+
if (tabBar != nil) {
105+
if (NativeScriptFabricViewIsDescendantOfView(tabBar, self)) {
106+
CGPoint tabBarPoint = [tabBar convertPoint:windowPoint fromView:self.window];
107+
UIView* tabBarHitView = [tabBar hitTest:tabBarPoint withEvent:event];
108+
if (tabBarHitView != nil) {
109+
return tabBarHitView;
110+
}
111+
}
112+
if (!NativeScriptFabricViewIsDescendantOfView(self, tabBar)) {
113+
return nil;
114+
}
115+
}
116+
117+
return hitView;
118+
}
119+
59120
- (void)updateProps:(Props::Shared const&)props oldProps:(Props::Shared const&)oldProps {
60121
const auto oldViewProps = std::static_pointer_cast<const NativeScriptUIViewProps>(_props);
61122
const auto newViewProps = std::static_pointer_cast<const NativeScriptUIViewProps>(props);

packages/react-native/ios/NativeScriptUIView.mm

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,42 @@ static id NativeScriptNSObjectFromHandle(NSString* handle) {
5050
return nil;
5151
}
5252

53+
static BOOL NativeScriptViewIsDescendantOfView(UIView* view, UIView* ancestor) {
54+
UIView* current = view;
55+
while (current != nil) {
56+
if (current == ancestor) {
57+
return YES;
58+
}
59+
current = current.superview;
60+
}
61+
return NO;
62+
}
63+
64+
static UITabBar* NativeScriptVisibleTabBarAtPoint(
65+
UIView* root,
66+
UIWindow* window,
67+
CGPoint windowPoint) {
68+
if (root.hidden || root.alpha <= 0.01 || !root.userInteractionEnabled) {
69+
return nil;
70+
}
71+
72+
if ([root isKindOfClass:UITabBar.class]) {
73+
CGPoint localPoint = [root convertPoint:windowPoint fromView:window];
74+
if ([root pointInside:localPoint withEvent:nil]) {
75+
return static_cast<UITabBar*>(root);
76+
}
77+
}
78+
79+
for (UIView* subview in [root.subviews reverseObjectEnumerator]) {
80+
UITabBar* tabBar = NativeScriptVisibleTabBarAtPoint(subview, window, windowPoint);
81+
if (tabBar != nil) {
82+
return tabBar;
83+
}
84+
}
85+
86+
return nil;
87+
}
88+
5389
@implementation NativeScriptUIView {
5490
UIView* _nativeView;
5591
UIView* _childrenView;
@@ -253,6 +289,31 @@ - (void)insertSubview:(UIView*)view atIndex:(NSInteger)index {
253289
[super insertSubview:view atIndex:index];
254290
}
255291

292+
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event {
293+
UIView* hitView = [super hitTest:point withEvent:event];
294+
if (hitView == nil || self.window == nil) {
295+
return hitView;
296+
}
297+
298+
CGPoint windowPoint = [self convertPoint:point toView:self.window];
299+
UITabBar* tabBar =
300+
NativeScriptVisibleTabBarAtPoint(self.window, self.window, windowPoint);
301+
if (tabBar != nil) {
302+
if (NativeScriptViewIsDescendantOfView(tabBar, self)) {
303+
CGPoint tabBarPoint = [tabBar convertPoint:windowPoint fromView:self.window];
304+
UIView* tabBarHitView = [tabBar hitTest:tabBarPoint withEvent:event];
305+
if (tabBarHitView != nil) {
306+
return tabBarHitView;
307+
}
308+
}
309+
if (!NativeScriptViewIsDescendantOfView(self, tabBar)) {
310+
return nil;
311+
}
312+
}
313+
314+
return hitView;
315+
}
316+
256317
- (void)didMoveToWindow {
257318
[super didMoveToWindow];
258319
[self mountUIKitHostIfNeeded];

packages/react-native/src/index.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,14 @@ export type UIKitViewRef<NativeView = unknown> = {
171171
invalidateNativeLayout: () => void;
172172
};
173173

174+
export type UIKitHostViewProps = ViewProps & {
175+
attachController?: boolean;
176+
};
177+
174178
export type UIKitViewComponent<Props extends object, NativeView = unknown> =
175179
ForwardRefExoticComponent<
176-
PropsWithoutRef<Props & ViewProps> & RefAttributes<UIKitViewRef<NativeView>>
180+
PropsWithoutRef<Props & UIKitHostViewProps> &
181+
RefAttributes<UIKitViewRef<NativeView>>
177182
>;
178183

179184
export type UIKitContainerResult<RootView = unknown, ChildrenView = unknown> = {

packages/react-native/src/index.ts

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,14 @@ export type UIKitViewRef<NativeView = unknown> = {
134134
invalidateNativeLayout: () => void;
135135
};
136136

137+
export type UIKitHostViewProps = ViewProps & {
138+
attachController?: boolean;
139+
};
140+
137141
export type UIKitViewComponent<Props extends object, NativeView = unknown> =
138142
ForwardRefExoticComponent<
139-
PropsWithoutRef<Props & ViewProps> & RefAttributes<UIKitViewRef<NativeView>>
143+
PropsWithoutRef<Props & UIKitHostViewProps> &
144+
RefAttributes<UIKitViewRef<NativeView>>
140145
>;
141146

142147
export type UIKitContainerResult<
@@ -356,11 +361,11 @@ const hostViewPropNames = new Set([
356361
]);
357362

358363
function splitUIKitViewProps<Props extends object>(
359-
props: Props & ViewProps,
364+
props: Props & UIKitHostViewProps,
360365
definition: UIKitViewDefinition<Props>,
361366
): {
362367
nativeProps: ViewProps;
363-
pluginProps: Props & ViewProps;
368+
pluginProps: Props & UIKitHostViewProps;
364369
} {
365370
const nativeProps: Record<string, unknown> = {};
366371
const pluginProps: Record<string, unknown> = {};
@@ -381,7 +386,7 @@ function splitUIKitViewProps<Props extends object>(
381386

382387
return {
383388
nativeProps: nativeProps as ViewProps,
384-
pluginProps: pluginProps as Props & ViewProps,
389+
pluginProps: pluginProps as Props & UIKitHostViewProps,
385390
};
386391
}
387392

@@ -1711,7 +1716,7 @@ function getObserverClass(): any {
17111716

17121717
function createUIKitContext<Props extends object>(
17131718
name: string,
1714-
propsRef: React.MutableRefObject<Props>,
1719+
propsRef: {current: Props},
17151720
invalidateLayout: () => void,
17161721
): UIKitRuntimeContext<Props> {
17171722
'worklet';
@@ -2052,7 +2057,7 @@ function defineUIKitHost<Props extends object, NativeView = unknown>(
20522057
|| definition.displayName
20532058
|| 'NativeScriptUIKitView';
20542059

2055-
const Component = forwardRef<UIKitViewRef<NativeView>, Props & ViewProps>(
2060+
const Component = forwardRef<UIKitViewRef<NativeView>, Props & UIKitHostViewProps>(
20562061
function NativeScriptUIKitView(props, ref) {
20572062
const {nativeProps, pluginProps} = splitUIKitViewProps(props, definition);
20582063
const createHost = definition.create;
@@ -2081,10 +2086,9 @@ function defineUIKitHost<Props extends object, NativeView = unknown>(
20812086

20822087
if (!initialRegistrationRef.current && initialErrorRef.current == null) {
20832088
try {
2084-
initialHostHandlesRef.current = runOnUISync(() => {
2089+
initialHostHandlesRef.current = runOnUISync((currentProps) => {
20852090
installUIKitNativeMountBridge();
20862091

2087-
const currentProps = propsRef.current;
20882092
const existingHost = uikitHostRegistry().get(hostId);
20892093
if (existingHost) {
20902094
existingHost.propsRef.current = currentProps;
@@ -2130,7 +2134,7 @@ function defineUIKitHost<Props extends object, NativeView = unknown>(
21302134
propsRef: pendingPropsRef,
21312135
});
21322136
return null;
2133-
});
2137+
}, pluginProps);
21342138
initialRegistrationRef.current = true;
21352139
if (initialHostHandlesRef.current != null) {
21362140
previousPropsRef.current = pluginProps;
@@ -2236,16 +2240,17 @@ function defineUIKitHost<Props extends object, NativeView = unknown>(
22362240
cancelled = true;
22372241
disposedRef.current = true;
22382242
mountedRef.current = false;
2239-
runOnUI(() => {
2240-
disposeRegisteredUIKitHost(hostId, propsRef.current);
2241-
}).catch((reason) => {
2243+
const disposeProps = propsRef.current;
2244+
runOnUI((currentProps) => {
2245+
disposeRegisteredUIKitHost(hostId, currentProps);
2246+
}, disposeProps).catch((reason) => {
22422247
setError(reason instanceof Error ? reason : new Error(String(reason)));
22432248
});
22442249
};
22452250
}
22462251

2247-
runOnUI(() => {
2248-
const currentProps = propsRef.current;
2252+
const effectProps = propsRef.current;
2253+
runOnUI((currentProps) => {
22492254
installUIKitNativeMountBridge();
22502255

22512256
const existingHost = uikitHostRegistry().get(hostId);
@@ -2293,15 +2298,16 @@ function defineUIKitHost<Props extends object, NativeView = unknown>(
22932298
propsRef: pendingPropsRef,
22942299
});
22952300
return createRegisteredUIKitHostFromNative(hostId);
2296-
})
2301+
}, effectProps)
22972302
.then((handles) => {
22982303
if (handles == null) {
22992304
throw new Error(`UIKit host ${hostId} was not created`);
23002305
}
23012306
if (cancelled || disposedRef.current) {
2302-
runOnUI(() => {
2303-
disposeRegisteredUIKitHost(hostId, propsRef.current);
2304-
}).catch((reason) => {
2307+
const disposeProps = propsRef.current;
2308+
runOnUI((currentProps) => {
2309+
disposeRegisteredUIKitHost(hostId, currentProps);
2310+
}, disposeProps).catch((reason) => {
23052311
setError(reason instanceof Error ? reason : new Error(String(reason)));
23062312
});
23072313
return;
@@ -2320,9 +2326,10 @@ function defineUIKitHost<Props extends object, NativeView = unknown>(
23202326
cancelled = true;
23212327
disposedRef.current = true;
23222328
mountedRef.current = false;
2323-
runOnUI(() => {
2324-
disposeRegisteredUIKitHost(hostId, propsRef.current);
2325-
}).catch((reason) => {
2329+
const disposeProps = propsRef.current;
2330+
runOnUI((currentProps) => {
2331+
disposeRegisteredUIKitHost(hostId, currentProps);
2332+
}, disposeProps).catch((reason) => {
23262333
setError(reason instanceof Error ? reason : new Error(String(reason)));
23272334
});
23282335
};
@@ -2344,17 +2351,17 @@ function defineUIKitHost<Props extends object, NativeView = unknown>(
23442351
const previousProps = previousPropsRef.current;
23452352
previousPropsRef.current = currentProps;
23462353

2347-
runOnUI(() => {
2354+
runOnUI((nextProps, fallbackPreviousProps) => {
23482355
const host = getRegisteredUIKitHost<NativeView>(hostId);
2349-
host.propsRef.current = currentProps;
2356+
host.propsRef.current = nextProps;
23502357
updateHost?.(
23512358
host.nativeView,
2352-
currentProps,
2353-
host.previousProps ?? previousProps,
2359+
nextProps,
2360+
host.previousProps ?? fallbackPreviousProps,
23542361
host.context,
23552362
);
2356-
host.previousProps = currentProps;
2357-
}).catch((reason) => {
2363+
host.previousProps = nextProps;
2364+
}, currentProps, previousProps).catch((reason) => {
23582365
setError(reason instanceof Error ? reason : new Error(String(reason)));
23592366
});
23602367
updateMeasuredSize();
@@ -2366,13 +2373,15 @@ function defineUIKitHost<Props extends object, NativeView = unknown>(
23662373
}
23672374

23682375
mountedRef.current = true;
2369-
runOnUI(() => {
2370-
if (!disposedRef.current) {
2376+
const currentProps = propsRef.current;
2377+
const isDisposed = disposedRef.current;
2378+
runOnUI((nextProps, shouldSkipMounted) => {
2379+
if (!shouldSkipMounted) {
23712380
const host = getRegisteredUIKitHost<NativeView>(hostId);
2372-
host.propsRef.current = propsRef.current;
2373-
mountedHost?.(host.nativeView, propsRef.current, host.context);
2381+
host.propsRef.current = nextProps;
2382+
mountedHost?.(host.nativeView, nextProps, host.context);
23742383
}
2375-
}).catch((reason) => {
2384+
}, currentProps, isDisposed).catch((reason) => {
23762385
setError(reason instanceof Error ? reason : new Error(String(reason)));
23772386
});
23782387
}, [hostId, mountedHost, nativeViewHandle]);
@@ -2390,15 +2399,16 @@ function defineUIKitHost<Props extends object, NativeView = unknown>(
23902399
: undefined;
23912400
const {children, ...nativePropsWithoutChildren} =
23922401
nativeProps as ViewProps & {children?: React.ReactNode};
2402+
const attachController = props.attachController !== false;
23932403

23942404
return React.createElement(NativeScriptUIViewNativeComponent, {
23952405
...nativePropsWithoutChildren,
23962406
collapsable: false,
23972407
children: childrenViewHandle ? children : undefined,
23982408
childrenViewHandle,
2399-
controllerHandle,
2409+
controllerHandle: attachController ? controllerHandle : undefined,
24002410
debugName,
2401-
hostId,
2411+
hostId: attachController ? hostId : undefined,
24022412
nativeViewHandle,
24032413
style: layoutStyle
24042414
? [nativeProps.style, layoutStyle]

0 commit comments

Comments
 (0)