@@ -20,22 +20,24 @@ of React Native internals and enabling fast, reproducible builds.
2020
2121``` bash
2222npx react-native init MyApp
23- cd MyApp/ios
24- open MyApp-SPM. xcodeproj
25- # Build and run from Xcode — no pod install, no Ruby toolchain
23+ cd MyApp
24+ npx react-native spm # auto-detects first-run → init; prompts to rename legacy CocoaPods xcodeproj
25+ npm run ios
2626```
2727
2828A future CLI integration (e.g., an ` --ios-build-system spm ` flag on
2929` react-native init ` ) could run ` react-native spm init ` automatically as part
30- of project creation.
30+ of project creation, eliminating the manual step .
3131
3232### Existing project
3333
3434``` bash
35- cd MyApp/ios
36- react-native spm init
37- # CocoaPods setup continues to work — both can coexist
38- open MyApp-SPM.xcodeproj
35+ cd MyApp
36+ npx react-native spm
37+ # Prompted: rename CocoaPods MyApp.xcodeproj → MyApp.xcodeproj.legacy?
38+ # Accept (Y) — the SPM xcodeproj writes to the now-free MyApp.xcodeproj slot,
39+ # `npm run ios` resolves to it unambiguously. The legacy stays on disk
40+ # (git mv tracks the rename cleanly) for rollback via `spm clean --project`.
3941```
4042
4143After initial setup, day-to-day development requires no extra commands. Adding
@@ -75,10 +77,14 @@ custom workarounds. First-class SPM support might help overcoming this barrier.
7577
7678### Compatibility
7779
78- CocoaPods continues to work during the transition period. The SPM workflow
79- generates a separate ` AppName-SPM.xcodeproj ` that coexists with the existing
80- Pods-based project. Teams can migrate at their own pace before CocoaPods trunk
81- goes read-only in December 2026.
80+ The SPM workflow generates an ` AppName.xcodeproj ` that takes the same
81+ filename slot as the legacy CocoaPods xcodeproj. On ` init ` , the script
82+ prompts to rename the existing CocoaPods project to ` AppName.xcodeproj.legacy `
83+ — preserving it for rollback while letting the community CLI's
84+ ` findXcodeProject ` resolve ` npm run ios ` to the SPM project unambiguously.
85+ Teams can migrate at their own pace before CocoaPods trunk goes read-only
86+ in December 2026, and ` spm clean --project ` reverses the migration when
87+ needed.
8288
8389## Detailed design
8490
@@ -99,42 +105,48 @@ goes read-only in December 2026.
99105└──────────────────┬──────────────────────────────┘
100106 │ symlink
101107 ▼
102- ┌─────────────────────────────────────────────────┐
103- │ App Root │
104- │ ├── Package.swift (committed) │
105- │ ├── AppName-SPM.xcodeproj/ (committed) │
106- │ ├── autolinked/ (generated) │
107- │ │ ├── Package.swift │
108- │ │ └── sources/ (symlinks) │
109- │ └── build/ │
110- │ ├── generated/ios/ (codegen) │
111- │ └── xcframeworks/ (symlinks) │
112- │ ├── Package.swift │
108+ ┌──────────────────────────────────────────────────┐
109+ │ App ios/ │
110+ │ ├── AppName.xcodeproj/ (committed) │
111+ │ │ └── .spm-managed (marker file) │
112+ │ ├── AppName.xcodeproj.legacy/ (committed if │
113+ │ │ rename was │
114+ │ │ accepted) │
115+ │ └── build/ │
116+ │ ├── generated/ │
117+ │ │ ├── autolinking/ (generated) │
118+ │ │ │ ├── Package.swift │
119+ │ │ │ ├── autolinking.json │
120+ │ │ │ └── packages/ (synth wrappers) │
121+ │ │ └── ios/ (codegen) │
122+ │ └── xcframeworks/ (symlinks) │
123+ │ ├── Package.swift │
113124│ ├── React.xcframework -> cache │
114125│ ├── ReactNativeDependencies.xcframework │
115126│ └── hermes-engine.xcframework │
116- └─────────────────────────────────────────────────┘
127+ └────────────────────────────────────────────────── ┘
117128```
118129
119130### Pipeline
120131
121- ` react-native spm ` orchestrates five steps (the underlying script is
132+ ` react-native spm ` orchestrates six steps (the underlying script is
122133` scripts/setup-apple-spm.js ` ):
123134
124135| # | Step | Script | Output |
125136| ---| ------| --------| --------|
126- | 1 | Codegen | ` generate-codegen-artifacts.js ` | ` build/generated/ios/ ` |
127- | 2 | Autolinking | ` spm/generate-spm-autolinking.js ` | ` autolinked/Package.swift ` + source symlinks |
128- | 3 | Download | ` spm/download-spm-artifacts.js ` | Cached xcframeworks |
129- | 4 | Package | ` spm/generate-spm-package.js ` | ` build/xcframeworks/Package.swift ` + symlinks |
130- | 5 | Xcodeproj | ` spm/generate-spm-xcodeproj.js ` | ` AppName-SPM.xcodeproj ` |
131- | — | Sync (build-time) | ` spm/sync-spm-autolinking.js ` | Re-runs steps 1–4 when inputs change (downloads artifacts if missing) |
132-
133- When run with the ` init ` action, the script also appends SPM-specific entries to the
134- project's ` .gitignore ` (e.g., ` autolinked/ ` , ` build/generated/ios/ ` ,
135- ` build/xcframeworks/ ` , ` .build/ ` , ` Package.resolved ` ). Entries that already
136- exist are not duplicated. This ensures generated artifacts are not accidentally
137- committed.
137+ | 1 | CLI config | ` spm/generate-spm-autolinking-config.js ` | ` build/generated/autolinking/autolinking.json ` |
138+ | 2 | Codegen | ` generate-codegen-artifacts.js ` | ` build/generated/ios/ ` |
139+ | 3 | Autolinking | ` spm/generate-spm-autolinking.js ` | ` build/generated/autolinking/Package.swift ` + source symlinks |
140+ | 4 | Download | ` spm/download-spm-artifacts.js ` | Cached xcframeworks |
141+ | 5 | Package | ` spm/generate-spm-package.js ` | ` build/xcframeworks/Package.swift ` + symlinks |
142+ | 6 | Xcodeproj | ` spm/generate-spm-xcodeproj.js ` | ` AppName.xcodeproj ` + ` .spm-managed ` marker (` init ` only; create-if-missing on subsequent runs) |
143+ | — | Sync (build-time) | ` spm/sync-spm-autolinking.js ` | Re-runs steps 1–5 when inputs change (downloads artifacts if missing) |
144+
145+ The ` init ` action additionally (a) prompts to rename any existing
146+ CocoaPods ` <App>.xcodeproj ` to ` <App>.xcodeproj.legacy ` before step 6, and
147+ (b) appends SPM-specific entries to ` .gitignore `
148+ (` build/generated/ ` , ` build/xcframeworks/ ` , ` .build/ ` , ` Package.resolved ` ).
149+ Existing entries are not duplicated.
138150
139151### Auto-sync build phase
140152
@@ -173,17 +185,26 @@ builds recover automatically.
173185### Cleaning generated SPM state
174186
175187Xcode's "Clean Build Folder" (Cmd+Shift+K) only removes DerivedData — it does
176- not touch the project's ` build/ ` , ` autolinked/ ` , or ` .build/ ` directories.
177- Xcode provides no hook to run custom scripts during GUI clean actions.
188+ not touch the project's ` build/ ` or ` .build/ ` directories. Xcode provides no
189+ hook to run custom scripts during GUI clean actions.
178190
179- To fully reset SPM state, run:
191+ ` react-native spm clean ` is scoped by opt-in flags. The default removes only
192+ generated dirs under ` appRoot ` :
180193
181194``` bash
182- react-native spm clean
195+ react-native spm clean # build/xcframeworks/, build/generated/, .build/
196+ react-native spm clean --project # also: delete SPM xcodeproj, restore .legacy backup
197+ react-native spm clean --derived-data # also: this app's Xcode DerivedData entries
198+ react-native spm clean --cache # also: cached xcframework slot for current version
199+ react-native spm clean --all # = --project --derived-data --cache
183200```
184201
185- This removes ` build/xcframeworks/ ` , ` build/generated/ios/ ` , ` autolinked/ ` , and
186- ` .build/ ` . Then run ` react-native spm update ` (or open the checked-in
202+ Destructive scopes (` --project ` , ` --derived-data ` , ` --cache ` , ` --all ` ) prompt
203+ for confirmation (bypass with ` --yes ` ). ` --project ` is the reverse of the
204+ init-time rename migration — deleting the SPM xcodeproj and restoring
205+ ` <App>.xcodeproj.legacy ` to its original filename if a backup exists.
206+
207+ After a plain ` clean ` , run ` react-native spm update ` (or open the checked-in
187208` .xcodeproj ` and build) to regenerate state. SPM package resolution is locked
188209for the duration of a build — if only stubs were left in place, Xcode would
189210resolve stubs and never pick up the real packages generated by the sync build
@@ -223,37 +244,34 @@ for CI environments where a specific path must be persisted across builds.
223244
224245### Package graph
225246
226- SPM resolves three local packages as dependencies of the app target:
247+ The generated ` <App>.xcodeproj ` references three local packages directly
248+ via ` XCLocalSwiftPackageReference ` — no app-level ` Package.swift ` is required:
227249
228250```
229- AppName-SPM.xcodeproj
230- └── Package.swift (app root — committed, user-owned)
231- ├── build/xcframeworks/Package.swift
232- │ ├── ReactNative (product, wraps React binaryTarget)
233- │ ├── ReactNativeDependencies (binaryTarget)
234- │ └── hermes-engine (binaryTarget)
235- ├── build/generated/ios/Package.swift
236- │ ├── ReactCodegen (target — codegen output)
237- │ └── ReactAppDependencyProvider (target)
238- ├── autolinked/Package.swift
239- │ └── <AutolinkedModule>... (targets — symlinked sources)
240- └── build/generated/ios/ (codegen source path)
251+ AppName.xcodeproj
252+ ├── XCLocalSwiftPackageReference → build/xcframeworks/Package.swift
253+ │ ├── ReactNative (product, wraps React binaryTarget)
254+ │ ├── ReactNativeDependencies (binaryTarget)
255+ │ └── hermes-engine (binaryTarget)
256+ ├── XCLocalSwiftPackageReference → build/generated/ios/Package.swift
257+ │ ├── ReactCodegen (target — codegen output)
258+ │ └── ReactAppDependencyProvider (target)
259+ └── XCLocalSwiftPackageReference → build/generated/autolinking/Package.swift
260+ └── <AutolinkedModule>... (targets — symlinked sources)
241261```
242262
243- The root ` Package.swift ` is generated once (` react-native spm init ` ) and committed. Developers
244- can customize it — add dependencies, adjust settings, or add new targets.
245- Subsequent runs only regenerate the sub-packages (` autolinked/ ` and
246- ` build/xcframeworks/ ` ).
247-
248- Both ` Package.swift ` and ` AppName-SPM.xcodeproj ` are generated once during
249- ` react-native spm init ` and committed. This is typically done by project
250- scaffolding tools (` react-native init ` , ` expo init ` ) rather than by developers
251- manually. After initial generation, these files are user-owned — subsequent
252- ` react-native spm update ` runs only regenerate the sub-packages, not the root
253- ` Package.swift ` or ` .xcodeproj ` . Teammates can clone the repo and open Xcode
254- immediately — stub packages allow package resolution to succeed, and the
255- auto-sync build phase downloads artifacts and handles autolinking on the first
256- build.
263+ These three sub-package paths are ** stable** : adding or removing community
264+ deps changes the contents of ` build/generated/autolinking/Package.swift `
265+ (gitignored) but never the xcodeproj's references. That's why the
266+ ` .xcodeproj ` is committed once and not regenerated on subsequent runs.
267+
268+ The xcodeproj generation is ** create-if-missing** on ` update ` (use
269+ ` --force-xcodeproj ` for an explicit overwrite). This protects user-side
270+ Xcode edits — signing, capabilities, Build Phases, scheme settings — from
271+ being clobbered. Teammates can clone the repo and open Xcode immediately:
272+ stub ` Package.swift ` files in each sub-package directory let SPM resolution
273+ succeed before the first build, and the auto-sync build phase downloads
274+ artifacts and writes the real sub-packages on first compile.
257275
258276### Header resolution
259277
@@ -458,14 +476,16 @@ and network access, the `.xcodeproj` could be eliminated in favor of a purely
458476SPM-native workflow — but this is a future direction that depends on Apple's
459477roadmap, not something this proposal can resolve.
460478
461- ### User-owned ` Package.swift `
479+ ### Committed xcodeproj edits
462480
463- The root ` Package.swift ` is generated once and committed. It only references
464- sub-packages by relative path, so React Native upgrades do not require changes
465- to it. However, developers who customize it (adding dependencies or targets)
466- must understand the generated package structure. In rare cases where the
467- sub-package layout changes across a major version, the root file may need
468- manual updates.
481+ The generated ` <App>.xcodeproj ` is committed and may carry user edits —
482+ signing, capabilities, Build Phases, custom schemes. The ` update ` action is
483+ ** create-if-missing** to protect those edits, which means the script does
484+ not propagate generator improvements into existing projects automatically.
485+ Bug fixes that change the emitted pbxproj need an explicit
486+ ` --force-xcodeproj ` run to take effect. A future improvement could
487+ preserve user-side edits through a merge step rather than full overwrite —
488+ see "Hardening ` update --force-xcodeproj ` " in unresolved questions.
469489
470490## Alternatives
471491
@@ -541,11 +561,18 @@ required; libraries that don't provide them fall back to source compilation.
541561
542562### Migration path for existing apps
543563
544- 1 . Run ` react-native spm init ` in the ` ios/ ` directory.
545- 2 . Commit the generated ` Package.swift ` and ` AppName-SPM.xcodeproj ` .
546- 3 . Open the new ` .xcodeproj ` and build.
547- 4 . Once validated, optionally remove CocoaPods files (` Podfile ` , ` Pods/ ` ,
548- ` .xcworkspace ` ).
564+ 1 . Run ` npx react-native spm ` from the project root (auto-redirects into
565+ ` ios/ ` ).
566+ 2 . Accept the rename prompt — your existing ` AppName.xcodeproj ` becomes
567+ ` AppName.xcodeproj.legacy ` (preserved for rollback).
568+ 3 . Commit the new ` AppName.xcodeproj/ ` (SPM-managed) and the renamed
569+ ` AppName.xcodeproj.legacy/ ` . ` git mv ` tracks the rename cleanly.
570+ 4 . Run ` npm run ios ` and verify the SPM build.
571+ 5 . Once validated, optionally delete the ` .legacy ` backup, ` Podfile ` ,
572+ ` Pods/ ` , and ` .xcworkspace ` .
573+
574+ To roll back: ` npx react-native spm clean --project ` deletes the SPM
575+ xcodeproj and renames ` .legacy ` back to the canonical filename.
549576
550577No changes to JavaScript code, Metro configuration, or Android setup are
551578required.
@@ -556,10 +583,10 @@ After upgrading `react-native` in `package.json` and running `npm install`,
556583the auto-sync build phase detects the ` node_modules ` mtime change on the next
557584Xcode build and re-runs the sync step automatically. This downloads the new
558585version's xcframeworks, regenerates the sub-packages, and updates autolinking.
559- No manual edits to ` Package.swift ` are needed — the root ` Package.swift ` only
560- references sub-packages by relative path , and those sub-packages are fully
561- regenerated each run. Developers can also run ` react-native spm ` manually to
562- trigger the update before building.
586+ No manual edits are needed — the xcodeproj's sub-package references are
587+ stable , and those sub-packages are fully regenerated each run. Developers
588+ can also run ` react-native spm ` manually to trigger the update before
589+ building.
563590
564591## How we teach this
565592
@@ -622,14 +649,14 @@ trigger the update before building.
622649 sources compile as an SPM target without producing a full release artifact.
623650 This would be useful for CI checks on pull requests.
624651
625- 5 . ** Hardening ` init ` for existing projects .** Running ` init ` on a project
626- that already has ` Package.swift ` or ` .xcodeproj ` should detect existing
627- files and avoid overwriting user modifications. The current ` .xcodeproj `
628- generation uses a simple template-based approach; adopting a proper Xcode
629- project parser/generator (e.g., ` @bacons/xcode ` ) would enable safer
630- incremental updates — reading the existing project, merging changes, and
631- writing back without losing manual edits. This is planned work for
632- production readiness.
652+ 5 . ** Hardening ` update --force-xcodeproj ` .** The default ` update ` is
653+ create-if-missing, which preserves user edits but means generator
654+ improvements don't propagate to existing projects automatically. Passing
655+ ` --force-xcodeproj ` clobbers everything. A future improvement could read
656+ the existing pbxproj, merge changes (signing, capabilities, custom Build
657+ Phases), and write back — likely via a proper Xcode project
658+ parser/generator (e.g., ` @bacons/xcode ` ) rather than the current
659+ template-based approach. Planned work for production readiness.
633660
6346616 . ** Auto-sync failure visibility.** The sync build phase currently emits
635662 ` warning: ` and exits 0 on failure, which means a broken autolinking state
0 commit comments