Description
The public header ios/utils/UIView+RNSUtility.h directly imports <React/RCTSurfaceTouchHandler.h> and exposes RCTSurfaceTouchHandler as a return type in a category interface:
// ios/utils/UIView+RNSUtility.h
#import <React/RCTSurfaceTouchHandler.h>
// ...
- (nullable RCTSurfaceTouchHandler *)rnscreens_findTouchHandlerInAncestorChain;
RCTSurfaceTouchHandler.h is not actually shipped in the React clang module — it lives in React-RCTFabric (RCTFabric.framework/Headers/React/RCTSurfaceTouchHandler.h). RNScreens' own xcconfig adds the RCTFabric framework search path so RNScreens compiles fine in isolation, but any consumer module that imports RNScreens (e.g. ExpoRouter) inherits this transitive header import without inheriting the RCTFabric search path. Under use_frameworks!:linkage => :static + new arch, Clang's module dependency scanning then fails before compilation even starts:
error: Clang dependency scanning failure: While building module 'RNScreens'
imported from RNScreens-6d5e763e.input:1:
UIView+RNSUtility.h:4:9 'React/RCTSurfaceTouchHandler.h' file not found
error: Compilation search paths unable to resolve module dependency: 'RNScreens'
(in target 'ExpoRouter' from project 'Pods')
The root cause is that a public header should not surface fabric-internal types as part of its module interface. A forward declaration in the header plus a real #import in the corresponding .mm is sufficient for RNScreens' own implementation, and keeps the public surface free of cross-module header dependencies. This matches the pattern already used by RNSScreenStack.mm and the sibling UINavigationBar+RNSUtility.h (which only imports <UIKit/UIKit.h> in its public header).
Steps to reproduce
- New Expo project on RN 0.85.3 with new arch (fabric-only) enabled.
- Add
expo-build-properties with:
{
"ios": { "useFrameworks": "static" }
}
- Install
react-native-screens@4.25.0-beta.1 and expo-router (or any module that imports the RNScreens clang module).
npx expo prebuild --clean && npx expo run:ios.
Snack or a link to a repository
Happy to put together a minimal repro repo if needed — the configuration is essentially bare-minimum Expo template + the three lines of expo-build-properties change above.
Screens version
4.25.0-beta.1 (also reproduces on current main — verified that ios/utils/UIView+RNSUtility.h still contains the direct #import <React/RCTSurfaceTouchHandler.h> at HEAD 9744779)
React Native version
0.85.3
Platforms
iOS
JavaScript runtime
Hermes
Workflow
Expo (bare / prebuild)
Architecture
Fabric (New Architecture)
Build type
Debug mode
Device
iOS Simulator
Acknowledgements
Yes
Suggested fix
Forward-declare in the header and move the #import to the implementation file:
// ios/utils/UIView+RNSUtility.h
#import <UIKit/UIKit.h>
@class RCTSurfaceTouchHandler; // instead of #import <React/RCTSurfaceTouchHandler.h>
// ios/utils/UIView+RNSUtility.mm
#import <React/RCTSurfaceTouchHandler.h> // moved from header
Note: <Foundation/Foundation.h> is replaced with <UIKit/UIKit.h> because RCTSurfaceTouchHandler.h was previously providing UIKit transitively, and the file declares a UIView category. This matches the convention used by the sibling header ios/utils/UINavigationBar+RNSUtility.h.
I verified locally that the change builds cleanly under use_frameworks!:static + new arch and unblocks consumer module dependency scanning.
All current internal consumers of UIView+RNSUtility.h (RNSScreenStack.mm, RNSScreen.mm, UIView+RNSUtility.mm itself) already #import <React/RCTSurfaceTouchHandler.h> directly, so the change is self-contained and does not introduce a new transitive-import requirement on existing call sites.
Related context
This is in the same family of use_frameworks!:static + new arch header-purity issues as expo/expo#39080 (where expo-router imports RNScreens internal headers). Different direction (consumer → RNScreens vs. RNScreens → React fabric), same underlying constraint: under static frameworks + Clang module dependency scanning, public-header transitive imports must resolve through the consumer's module search paths, not the producer's.
It is also conceptually adjacent to #2306 / #2319 (RectUtil.h import path), where moving an import to a fabric-renderer path resolved a similar consumer-side compilation failure under static frameworks.
Patch diff
diff --git a/ios/utils/UIView+RNSUtility.h b/ios/utils/UIView+RNSUtility.h
--- a/ios/utils/UIView+RNSUtility.h
+++ b/ios/utils/UIView+RNSUtility.h
@@ -1,7 +1,8 @@
#pragma once
-#import <Foundation/Foundation.h>
-#import <React/RCTSurfaceTouchHandler.h>
+#import <UIKit/UIKit.h>
+
+@class RCTSurfaceTouchHandler;
NS_ASSUME_NONNULL_BEGIN
diff --git a/ios/utils/UIView+RNSUtility.mm b/ios/utils/UIView+RNSUtility.mm
--- a/ios/utils/UIView+RNSUtility.mm
+++ b/ios/utils/UIView+RNSUtility.mm
@@ -1,5 +1,6 @@
#import "UIView+RNSUtility.h"
+#import <React/RCTSurfaceTouchHandler.h>
#import <React/RCTSurfaceView.h>
#import "RNSModalScreen.h"
Description
The public header
ios/utils/UIView+RNSUtility.hdirectly imports<React/RCTSurfaceTouchHandler.h>and exposesRCTSurfaceTouchHandleras a return type in a category interface:RCTSurfaceTouchHandler.his not actually shipped in theReactclang module — it lives inReact-RCTFabric(RCTFabric.framework/Headers/React/RCTSurfaceTouchHandler.h). RNScreens' own xcconfig adds theRCTFabricframework search path so RNScreens compiles fine in isolation, but any consumer module that imports RNScreens (e.g. ExpoRouter) inherits this transitive header import without inheriting theRCTFabricsearch path. Underuse_frameworks!:linkage => :static+ new arch, Clang's module dependency scanning then fails before compilation even starts:The root cause is that a public header should not surface fabric-internal types as part of its module interface. A forward declaration in the header plus a real
#importin the corresponding.mmis sufficient for RNScreens' own implementation, and keeps the public surface free of cross-module header dependencies. This matches the pattern already used byRNSScreenStack.mmand the siblingUINavigationBar+RNSUtility.h(which only imports<UIKit/UIKit.h>in its public header).Steps to reproduce
expo-build-propertieswith:{ "ios": { "useFrameworks": "static" } }react-native-screens@4.25.0-beta.1andexpo-router(or any module that imports theRNScreensclang module).npx expo prebuild --clean && npx expo run:ios.Snack or a link to a repository
Happy to put together a minimal repro repo if needed — the configuration is essentially
bare-minimumExpo template + the three lines ofexpo-build-propertieschange above.Screens version
4.25.0-beta.1 (also reproduces on current
main— verified thatios/utils/UIView+RNSUtility.hstill contains the direct#import <React/RCTSurfaceTouchHandler.h>at HEAD9744779)React Native version
0.85.3
Platforms
iOS
JavaScript runtime
Hermes
Workflow
Expo (bare / prebuild)
Architecture
Fabric (New Architecture)
Build type
Debug mode
Device
iOS Simulator
Acknowledgements
Yes
Suggested fix
Forward-declare in the header and move the
#importto the implementation file:Note:
<Foundation/Foundation.h>is replaced with<UIKit/UIKit.h>becauseRCTSurfaceTouchHandler.hwas previously providing UIKit transitively, and the file declares aUIViewcategory. This matches the convention used by the sibling headerios/utils/UINavigationBar+RNSUtility.h.I verified locally that the change builds cleanly under
use_frameworks!:static+ new arch and unblocks consumer module dependency scanning.All current internal consumers of
UIView+RNSUtility.h(RNSScreenStack.mm,RNSScreen.mm,UIView+RNSUtility.mmitself) already#import <React/RCTSurfaceTouchHandler.h>directly, so the change is self-contained and does not introduce a new transitive-import requirement on existing call sites.Related context
This is in the same family of
use_frameworks!:static+ new arch header-purity issues as expo/expo#39080 (whereexpo-routerimports RNScreens internal headers). Different direction (consumer → RNScreens vs. RNScreens → React fabric), same underlying constraint: under static frameworks + Clang module dependency scanning, public-header transitive imports must resolve through the consumer's module search paths, not the producer's.It is also conceptually adjacent to #2306 / #2319 (
RectUtil.himport path), where moving an import to a fabric-renderer path resolved a similar consumer-side compilation failure under static frameworks.Patch diff