Skip to content

[iOS] UIView+RNSUtility.h leaks RCTSurfaceTouchHandler import, breaks Clang module dependency scanning under use_frameworks!:static #3985

@zhe-qi

Description

@zhe-qi

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

  1. New Expo project on RN 0.85.3 with new arch (fabric-only) enabled.
  2. Add expo-build-properties with:
    {
      "ios": { "useFrameworks": "static" }
    }
  3. Install react-native-screens@4.25.0-beta.1 and expo-router (or any module that imports the RNScreens clang module).
  4. 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"

Metadata

Metadata

Assignees

Labels

platform:iosIssue related to iOS part of the libraryrepro-providedA reproduction with a snack or repo is provided

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions