RFC: (iOS) Swift Package Manager in React Native (Cocoapods deprecation) - #994
RFC: (iOS) Swift Package Manager in React Native (Cocoapods deprecation)#994chrfalch wants to merge 5 commits into
Conversation
mrousavy
left a comment
There was a problem hiding this comment.
Overall this is a really exciting!
I added a section about Mixing C++ and Swift with the direct interop, used by Nitro and all Nitro libraries as this is quite relevant to the ecosystem imo.
I'm sure I can find a solution for this soon.
Thanks for the great RFC Christian!
| ### Build from source | ||
|
|
||
| Build React Native from source will be supported in the final version of the SPM package system, but the current POC does not support it. | ||
|
|
There was a problem hiding this comment.
| ### Mixing C++ and Swift | |
| As per [the "Setting Up Mixed-Language Swift and C++ Projects" Swift documentation](https://www.swift.org/documentation/cxx-interop/project-build-setup/#mixing-swift-and-c-using-swift-package-manager), Swift Package Manager (SPM) **does not yet support Swift and C++ in the same target.** | |
| This causes issues for Swift + C++ libraries or frameworks, including but not limited to the entire [Nitro Modules](https://nitro.margelo.com) ecosystem, as Nitro is built ontop of the direct Swift + C++ interop feature, and uses calls from Swift -> C++, as well as calls from C++ -> Swift within the same target/library - aka bi-directional interop. | |
| This should not impose a problem for react-native directly as react-native uses Objective-C(++) for interoping between the languages (even Swift - C++ interop uses Objective-C(++) as a bridge, not **direct** Swift C++ interop). | |
| The [Margelo](https://margelo.com) team is investigating whether this can be solved easily in Nitro - e.g. by using separate targets (one C++, one Swift, and possibly a third one to somehow break the bi-directional dependency). | |
| As of today, this will affect popular libraries like react-native-vision-camera, react-native-mmkv, react-native-unistyles, react-native-video, and more. | |
I'll make sure to find a solution for this soon.
Saadnajmi
left a comment
There was a problem hiding this comment.
Thank you so much for this! I look forward to the new future. I left some minor comments and nits, but this looks very well thought out 👍
|
|
||
| This keeps the developer-facing model close to today’s React Native workflow: dependencies are still discovered from the JavaScript project, but the integration backend is SPM instead of CocoaPods. | ||
| Local modules | ||
| The prototype also supports additional local modules through `spmModules` in `react-native.config.js`. This makes it possible to include native code that is not otherwise discovered through standard autolinking. |
There was a problem hiding this comment.
One edge case that used to not work but I think does now is a monorepo with several library packages, and one app. Old autolinking used to not work and I would hardcode extra pods in my podfile that pointed to the "local" podspecs, but nowadays autolinking works with local packages too. It would be great if that kept working with SPM too.
There was a problem hiding this comment.
You will have a separate Package.swift files that you can commit and change as much as you like - it will reference another Package.swift file that contains the auto linked targets.
| After initialization, autolinking is normally kept up to date automatically by the generated Xcode project. The prototype adds an auto-sync build phase that detects dependency changes and regenerates the autolinked package before compilation when needed. | ||
|
|
||
| When a manual refresh is needed, for example after changing native dependencies or local module configuration, developers can re-run: |
There was a problem hiding this comment.
Is the idea that if someone adds a node module with native code, there's a script phase somewhere that detects that the user might be missing an autolinked pakage?
I.E:
yarn ios --init
yarn add @shopify/react-native-skia
# User runs and builds the app without running `yarn ios`
Would there be an error, or does the "auto-sync build phase" regenerate autolinked/Package.Swift?? I suppose this is similar to how RN devs know they need to pod install constantly, and much better than adding a post install.
There was a problem hiding this comment.
As for now the flow will basically be the same, when you update your dependencies you will need to update. This can of course be improved - the problem today is that running pod install is very slow - otherwise we'd be doing it automatically I guess.
|
|
||
| That mechanism is enough to prove that source-based native modules can be integrated into the SPM flow today. | ||
|
|
||
| For third-party libraries published to npm, the longer-term direction should be to add SPM metadata alongside the existing React Native autolinking metadata in `react-native.config.js`. That keeps React Native’s dependency discovery in one place and lets the CLI/autolinking pipeline decide how to integrate each library. |
There was a problem hiding this comment.
To confirm, autolinking would not do what the CLI does today with cocoapods and look for Package.Swift files at the root of every node module? That seems fine, just wanted to confirm.
There was a problem hiding this comment.
@Saadnajmi - Not sure what you're asking for here? We will basically try to use the same logic for auto linking since it is agnostic when it comes to wether we're using Cocoapods or SPM - it is all about collecting the modules that needs to be linked.
|
|
||
| ## Brownfield | ||
|
|
||
| Brownfield projects will also be able to use Package.swift files and integrate easily with existing Swift or Objective-C based apps, either as precompiled XCFrameworks or as source - both defined in a Package.swift file. There will be an option in the cli for generating separate Package.swift files for distributing the app as a brownfield target. |
There was a problem hiding this comment.
There will be an option in the cli for generating separate Package.swift files for distributing the app as a brownfield target.
Is this intended to support a similar use-case we previously did with Podfile using the following code?
target 'BrownfieldLib' do
inherit! :complete
endContext: In brownfield apps, when we create a new framework target inside a RNApp, we add the above block in RNApp/ios/Podfile to inherit the package linking and all to the framework target.
Since this behavior may not directly be available with SPM - I am wondering whether the suggested approach of creating a separate Package.swift for the framework target will have duplications containing the same parts as App Target's Package.swift file.
How are we planning to support this with SPM? With cocoapods, we would usually change this line to toggle between static and dynamic framework linkage: Since with the final shape of SPM, we may not have this Podfile - so I wonder have you thought of any ideas on how we support it? This in general should be for both greenfield and brownfield. |
This is rather third party library scoped https://github.com/callstack/react-native-brownfield. There we have a few brownfield packages and 3 of them are shipped as separate xcframeworks. The user then have the flexibility to either:
We do not provide first class support for SPM but that is doable by the user. Now that this RFC is actually going to make that support in the core, we will also comply and extend it to our brownfield packaging. This does NOT in anyway means that the generated SPM from this package needs to be linked by the Brownfield Target - rather by the native host App consuming it. One point on that:
The above is the layout of what I think we can do once we have this RFC delivered as a RC. Before that, we can also experiment with SPM and once RFC is released, we can change the local link for Brownfield Target to the Package.swift link and keep the rest Package.swift the same with binaryTargets. |
tjzel
left a comment
There was a problem hiding this comment.
Thanks for the well-thought RFC, I like this direction.
I haven't had the opportunity to use SPM but I wonder if libraries can comfortably hook some scripts prior to compilation using it.
Libraries like Reanimated and Worklets currently execute some custom Ruby scripts inside .podspec to assert proper configuration and obtain compilation flags.
It would be perfect if library authors could extract their common logic from build.gradle and .podspec and put it into react-native.config.js file but I'm aware it's out of scope of this RFC.
|
|
||
| ### Build from source | ||
|
|
||
| Build React Native from source will be supported in the final version of the SPM package system, but the current POC does not support it. |
There was a problem hiding this comment.
Does it mean that stepping into RN code while debugging will be only available when using Cocoapods, during the transition period?
There was a problem hiding this comment.
Until we enable building from source with SPM, yes, you'll have to go back and use Cocoapods, unfortunately.
There was a problem hiding this comment.
It doesn't mean that we will never support it - we just have to set a few limits to our 1 phase scope.
|
@tjzel I think you should be able to run Swift PM Plugins to run prebuild code and ensure that everything is properly configured. That's a good point to add to the RFC, that libraries should be able to ship and configure Swift PM plugins. @chrfalch I believe it is ok if the first implementation doesn't have it, and we work with you folks from Reanimated to add the feature! 😉 |
|
@tjzel and @cipolleschi: Build plugins have limited access to what they can do - but package resolving runs the Package.swift code and can contain almost anything you need, including updating script files or running separate pipelines. This is the equivalent of running pod install and will probably be the most popular hook for doing things like you mention. |
A package.swift file can include references to both prebuilt XCFrameworks and source files - so as long as the package you want to consume (like Firebase packages) is packaged as a framework you should be able to consume it from your package - without having to change anything in your project. |
| - runs React Native Codegen, | ||
| - generates `autolinked/Package.swift`, | ||
| - downloads the required prebuilt XCFramework artifacts, | ||
| - generates the local SPM packages consumed by the app, and | ||
| - creates an Xcode project configured for the SPM flow. |
There was a problem hiding this comment.
Will there be any hooks for customizations that require running scripts?
There was a problem hiding this comment.
Great idea, we haven't made any decisions about this yet - can you come up with some use cases for us to look into? (Remember that you will be able to run scripts from the resolve phase of your own package.swift code)
There was a problem hiding this comment.
In RNTA, we also generate the Xcode project files when pod install is run. Additionally, we also:
- Disable warnings in third party libraries to reduce noise
- Define macros to enable/disable certain parts in third party libraries
- Add assets/resources declared in an external file
- Configure code signing (also declared in an external file)
- Apply config plugins (if applicable)
- Various scripts to validate configuration files
- Generate code that must be included in the project
tibbe
left a comment
There was a problem hiding this comment.
Thanks for putting this together!
What's the story for migrating existing (non-framework) setups that already have a generated XCode project, etc to the new setup?
Add a ZELLO_USE_SPM=1 opt-in to the podspec: when set, ZelloSDK is pulled via Swift Package Manager (github.com/zelloptt/ios-mobile-sdk, product ZelloSDKUmbrella, up-to-next-major from 2.0.0) using RN's spm_dependency helper, instead of the CocoaPods 'pod ZelloSDK'. CocoaPods stays the default. Full CocoaPods removal isn't possible on RN 0.86 (RN itself still requires CocoaPods; SPM-only is an unshipped RN RFC, react-native-community/discussions-and-proposals#994), so this mirrors sentry-react-native's SENTRY_USE_SPM approach: SPM for the native SDK, CocoaPods for the RN layer. Verified: ZELLO_USE_SPM=1 pod install + xcodebuild resolves ZelloSDK 2.0.5 from SPM and the example builds, launches, and the SDK initializes - with no resilient-symbol dyld workaround needed (SPM binaries are prebuilt resiliently). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Hi |
Could you give a simple reproduction for this so that we could learn more? |
|
Hi @chrfalch — thanks for looking into this. Our React Native plugin's SPM support isn't released yet, so I can't point you at a published package that wires this up. But you don't need it — the crash is a pure Xcode/SPM/dyld issue (not React Native specific), and everything needed to reproduce is already public. Here's a compact spec you can hand to an AI/agent. BugA dynamic framework resolved via SPM inside a subproject (e.g. Reproduction (no React Native, no account, no unreleased code)Our native SDK is a public, standard SPM binary package (a
Expected: app launches. Constraints that must hold (otherwise the bug hides)
Root causePure CocoaPods embeds dynamic frameworks via its generated RFC implicationDeclaring the SPM dependency isn't sufficient on its own — there needs to be a defined embed + sign story for dynamic frameworks resolved in subprojects, otherwise every dynamic-framework SDK will crash on device. Let me know if a clone-and-run repro repo would help and I'll put one together. |
|
@MikePendo - The good news is that it comes from the hybrid layout: the framework being resolved on a subproject (Pods.xcodeproj) rather than on the app target. As you note in step 4, attaching it directly to the app target with Embed & Sign "auto-fixes it and hides the bug" — and that's exactly the configuration this RFC produces. add injects the SPM package references directly into the app's existing .xcodeproj / app target, in place, and add --deintegrate runs pod deintegrate and removes React Native from the Podfile, so in the end state there's no Pods.xcodeproj subproject holding the dependency. Xcode's automatic embed-and-sign for products attached to the app target is what covers the dynamic-framework case here. Where your point does land squarely is brownfield and the migration window, where an SPM-resolved dynamic framework can still live in a subproject alongside an existing CocoaPods/Xcode setup. I'll capture "embed + sign story for dynamic frameworks resolved in subprojects" as an explicit requirement for the brownfield/migration tooling so we don't regress into this on device. A clone-and-run repo would still be genuinely useful there — if you're up for putting one together, please do. |
Summary:⚠️ This is a stacked PR — review/merge the base PRs first Builds on, and should land after (bottom-up): 1. #57285 — Remove the Clang VFS overlay / modularize React headers (base of the stack). <React/…> via the framework module map; lowercase namespaces (react/, yoga/, jsi/, …) via the ReactNativeHeaders module map. 2. #57305 — Prebuilt artifact resources + ReactNativeHeaders.xcframework. Ships the prebuilt core artifacts (incl. ReactNativeHeaders in the tarball + embedded React.framework resources) and the CocoaPods React-Core-prebuilt facades. 3. #57440 — Self-serving prebuilt ReactNativeDependencies + dependency-only facades. The deps pod becomes the single header authority for the third-party namespaces (RCT-Folly/glog/boost/…); community `s.dependency "RCT-Folly"` resolves to local facades instead of trunk source pods. 4. #57442 — ReactNativeDependenciesHeaders sidecar + pure-RN ReactNativeHeaders (immediate base). The deps prebuild emits a headers-only LIBRARY-type sidecar (the binary deps xcframework is framework-type — invisible to SwiftPM binaryTargets), ReactNativeHeaders drops the third-party namespaces, and both headers artifacts publish standalone to Maven. The whole stack is rebased onto current main (2026-07-06). This PR consumes the final **five-artifact set**: React, ReactNativeHeaders (pure-RN), ReactNativeDependencies, ReactNativeDependenciesHeaders, hermes-engine. Adds npx react-native spm and the package-generation tooling that turns an app into a SwiftPM-integrated RN app using the base stack's prebuilt XCFrameworks. CocoaPods stays supported — additive, opt-in, no Ruby toolchain. Integration is injected into the existing .xcodeproj in place (nothing generated/renamed/replaced), recorded in .spm-injected.json so it reverses exactly. ### What this PR adds - The spm CLI (add/update/deinit/scaffold + hidden sync/codegen/download), zero-arg auto-resolution, --deintegrate for CocoaPods→SwiftPM. - SwiftPM package generation (scripts/spm/): autolinking→Package.swift, Codegen→React-GeneratedCode, core XCFramework binary targets, artifact download/cache, surgical pbxproj inject/remove. - ReactNativeDependenciesHeaders wired as the 5th binaryTarget: the headers-only companions (ReactNativeHeaders from the core tarball, the deps sidecar from the deps tarball) are staged automatically out of their parent tarballs — no --headers-tarball priming; REQUIRED_ARTIFACTS covers all five. - Community-library scaffolding from podspecs (incl. root-level-source podspecs via generated include/<SwiftName>/ shims, sibling wiring for transitive spm.dependencies, self-ingestion guards). - In-place Xcode integration (XCLocalSwiftPackageReference + Sync build phase + scheme pre-action + auto-sync; ${PODS_ROOT}-anchored REACT_NATIVE_PATH replaced on --deintegrate with exact deinit rollback). - rn-tester + helloworld SwiftPM consumption + test library; npm-packaging hygiene; a small RNCoreFacades.podspec_dir fix. ## Documentation - **Tool docs** (usage, quick start, architecture): [`scripts/spm/__doc__/spm-scripts.md`](https://github.com/react/react-native/blob/chrfalch/swift-package-manager/packages/react-native/scripts/spm/__doc__/spm-scripts.md) - **Header-paths contract** (how the five artifacts serve headers): [`scripts/spm/__doc__/spm-header-paths-contract.md`](https://github.com/react/react-native/blob/chrfalch/swift-package-manager/packages/react-native/scripts/spm/__doc__/spm-header-paths-contract.md) - **RFC** (updated): [react-native-community/discussions-and-proposals#994](react-native-community/discussions-and-proposals#994) ## Changelog: [IOS] [ADDED] - `npx react-native spm` command + SwiftPM package-generation tooling (opt-in; CocoaPods stays supported) Pull Request resolved: #57332 Test Plan: 350 scripts/spm unit tests (incl. byte-identical add→deinit round-trip); manual E2E against the five-artifact set: fresh app via `cli init`→`spm add --deintegrate`→build (artifact resources verified in the app), rn-tester in-place migration (RNTesterPods.xcodeproj, test libraries + spmModules), helloworld in-place migration — all BUILD SUCCEEDED. Verified `npx react-native spm <cmd>` from the command line in each journey. ## Scope & limitations iOS, prebuilt-only (no build-from-source yet); full spm.xcframework/spm.source library metadata not yet (app-local spm.modules + scaffolding are); Expo not yet. ## Follow-ups Remote-mode ReactNative Package.swift must vend the ReactNativeDependenciesHeaders product (spm-distribution repo); library self-containment for repo-portable manifests; build-from-source. Reviewed By: mdvacca Differential Revision: D111449548 Pulled By: cipolleschi fbshipit-source-id: 87a873bf5c8be2d60f7893611e16924bfa730a46
This RFC contains a plan and technical description for how to deprecate Cocoapods and replace it with Swift Package Manager on iOS.
RFC Document