6 min read · September 2026

29% of the top React Native libraries mix Swift and Objective-C. SwiftPM can't compile that.

We scanned the 300 most-installed React Native libraries with a native iOS component. 68% can have a Swift package manifest generated automatically. 3% already ship one. 29% cannot be auto-migrated at all — and every single one of them fails for exactly the same reason.

Stacked bar showing SwiftPM readiness across 283 React Native libraries: 68% scaffoldable, 29% manual, 3% ready.

The deadline is real, but it isn't what people think

CocoaPods trunk goes permanently read-only on December 2, 2026, with a test run of read-only mode on November 1–7. After that the central registry stops accepting new pod versions and new podspecs. Apps depending on existing pod versions keep building and running.

So nothing breaks on December 3rd. What stops is new releases, and the vendors moved first: Firebase stops publishing new Apple SDK versions to CocoaPods in October 2026, and Google said it would end CocoaPods support for its iOS SDKs after Q2 2026 in favour of Swift Package Manager.

The failure mode isn't a build error. It's a security patch you can't take, six months from now, in a transitive native dependency you didn't know you had.

What React Native actually ships today

React Native 0.87 added experimental Swift Package Manager support as an alternative to CocoaPods on iOS. It's opt-in and additive, CocoaPods remains the default supported path, and the SwiftPM path consumes the same prebuilt XCFrameworks React Native already publishes. No Ruby, no Bundler, no CocoaPods — just Xcode.

cd ios
npx react-native spm -- deintegrate

It injects Swift package references into your existing .xcodeproj rather than replacing it, so signing, capabilities and build phases stay untouched, and npx react-native spm deinit reverses it exactly. After a fresh clone and in CI you run npx react-native spm once before building — the analog of pod install. Commands, flags and generated layout may still change, and it's explicitly not for production yet.

The load-bearing sentence is the next one: a community library must ship a Package.swift, and if it doesn't, npx react-native spm scaffold generates one from the podspec.

That scaffold command is the whole ballgame. So we measured how often it works.

How we measured it

Our script doesn't reimplement podspec parsing. It loads React Native's own scripts/spm/read-podspec.js out of the target app's node_modules and uses that — the module that evaluates a podspec's Ruby DSL through pod ipc spec, patches out Podfile-only helpers like install_modules_dependencies(s) first so evaluation doesn't fail, falls back to a regex parser when CocoaPods isn't installed, and flattens subspecs. Roughly 800 lines of accumulated edge cases.

Reimplementing that would drift from what react-native spm scaffold actually does, and a classifier that disagrees with the tool it's predicting is worthless. The mixed-language gate in particular mirrors RN's own scaffolder check exactly, verified against 0.87.1 source.

Four buckets:

Status Count Share
scaffoldable 193 68%
manual 82 29%
ready 8 3%
blocked 0 0%
unclassifiable 17 6% of the run

283 classifiable libraries, from a 300-library run plus one spotlight. The 17 errors are excluded from the percentages rather than quietly bucketed.

Two numbers are worth sitting with.

Only 8 of 283 already ship a manifest. Three percent, a month after npx react-native spm landed in a stable release. The ecosystem has essentially not started.

The 29% holds when weighted by usage. The comfortable story would be that abandoned packages drag the average down. The data says otherwise: the manual bucket is 47 million of the sample's 165.5 million weekly downloads, or 28%. The hard libraries are the ones people actually install.

One cause, not a hundred

Here's what surprised us. The manual bucket isn't a long tail of exotic podspec features. It's one check:

if (report.hasSwift && report.hasClang) {
  report.status = 'manual';
  // 'skipped-mixed-language'
}

SwiftPM cannot compile Swift and Objective-C/C++ in a single target the way CocoaPods does. Every library whose source_files glob matches both a .swift and an .m/.mm/.c/.cpp is unscaffoldable, full stop. All 82.

The fix is a real refactor of the library's native layout: split the Swift shim into its own target and have it import the Objective-C half under #if SWIFT_PACKAGE. No generator does that for you, because it's a judgement call about module boundaries.

This isn't theoretical or marginal. RN 0.87's SwiftPM path is not yet compatible with the mixed-language native target Nitro Modules requires, so Nitro-based libraries stay on CocoaPods for now.

A single architectural mismatch, mechanically detectable, accounting for 28% of installs. That's unusually tractable as ecosystem problems go — and it means a registry of hand-written two-target manifests would move the whole ecosystem at once.

What we found in the podspecs along the way

Scanning 300 real packages surfaces things you can't find by reading the RFC.

An upstream bug in React Native's own podspec reader. @react-native-clipboard/clipboard has no top-level s.source_files. It declares s.ios.source_files, s.osx.source_files and s.visionos.source_files — CocoaPods' per-platform attribute syntax, which pod ipc spec resolves by nesting each value under its platform key. React Native's flattener only ever reads the top level, so on both 0.87-stable and main the model comes back with zero source files for a perfectly valid podspec.

We ran React Native 0.87.1's own scaffold code against the published clipboard 1.16.3 to see what happens next. It doesn't fail closed. It emits a target with path: "." and no sources, no publicHeadersPath, no header search paths — and with no source list, SwiftPM compiles whatever it finds in the package root. For clipboard that's the iOS implementation, plus the macOS one — a second @implementation RNCClipboard, written against AppKit's NSPasteboard — plus four Windows C++ files whose precompiled header includes <windows.h>, a header the iOS SDK doesn't have.

Expo modules hide their podspecs. They nest them under ios/, named nothing like the npm package — expo-constants ships ios/EXConstants.podspec. A root-only search finds nothing and reports a false negative.

Podspecs contain globs that Ruby accepts and nothing else does. react-native-vision-camera writes "ios/**/*.{swift}" — a single-item brace group. Dir.glob handles it; fast-glob treats {swift} as four literal characters and matches nothing. That silently zeroed out a real 175-file match during an end-to-end run, which would have flipped a manual verdict to a bogus error.

What this means for you

If you ship an app. Upgrade to 0.87 and try the SwiftPM path now — on a branch. It costs less than it sounds: npx react-native spm adds packages to your existing .xcodeproj rather than replacing it, and npx react-native spm deinit reverses it exactly, so a failed afternoon costs you nothing. What you get back is finding out which of your dependencies break while there's still time to do something about it. The clipboard bug above only turned up because we ran React Native's own scaffold code against real packages; your dependency tree has its own version of that waiting. Run the scan first so you know how many of your libraries are mixed-language, then try the switch and file what breaks. Every issue reported in September gets fixed before December instead of discovered after it. Just keep it off your release branch for now — the core team is explicit that the SwiftPM path isn't production-ready yet, and 0.87.1 alone carried fixes for Hermes bytecode mismatches in Release builds.

If you maintain a library. Run the check yourself: does your source_files glob match both .swift and .m/.mm? If not, you are one npx react-native spm scaffold away from done — ship the manifest, list it in your package's files array, and note that the config now lives in swiftpmConfig in package.json (the spm block in react-native.config.js is deprecated). If yes, you owe your users a target split, and it's better done deliberately than under deadline. Either way it's additive: CocoaPods integration is unchanged, there's no flag day, no coordination problem.

For React Native. Removing Ruby from the iOS toolchain is the biggest simplification the platform has had in years, and it lines up with precompiled binaries by default, the New Architecture as standard, and Hermes V1. Expo SDK 58 has restructured its iOS sources so Swift, Objective-C and C++ compile as separate SwiftPM targets, with an autolinking plugin for npx react-native spm in review — which is precisely the target split every mixed-language library now needs, done once at the framework level. The core is ready. The ecosystem is at 3%.

Caveats, honestly

A manual verdict can be wrong for binary distributions. expo-constants's podspec conditionally skips compiling sources entirely in favour of a prebuilt .xcframework when one is present. A fresh scan never has that binary on disk, so it always evaluates the other branch — and CocoaPods only reports the taken branch, so the resolved spec doesn't even mention vendored_frameworks. We flag any podspec whose text mentions one, but a library shipping a binary framework sidesteps the single-target-language limit entirely. Some share of the 82 may be fine in practice.

Some of the 17 errors are our problem, not the libraries'. At least one is the RN reader bug above.

This predicts whether a manifest can be generated, not whether the resulting build links, runs and archives. That's a harder question and a different tool.


← All posts