Skip to content

feat(analytics): support Swift Package Manager on React Native 0.88+ - #471

Open
ketanmixpanel wants to merge 13 commits into
masterfrom
feat/spm-rn-0.88
Open

ketanmixpanel wants to merge 13 commits into
masterfrom
feat/spm-rn-0.88

Conversation

@ketanmixpanel

@ketanmixpanel ketanmixpanel commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds Swift Package Manager (SPM) support to the iOS native module, so apps on React Native 0.88+ can use React Native's SPM autolinking instead of CocoaPods. The podspec still works, so existing CocoaPods apps need no changes.

Changes

Library

  • New ios/Package.swift, which React Native's autolinker picks up through the swiftpmConfig entry in package.json. SPM can't compile Swift and Objective-C in one target, so the module is split in two:
    • MixpanelReactNativeSwift: the Swift implementation. It depends on mixpanel-swift 7.0.0 and React's header products.
    • MixpanelReactNative: the Objective-C file that registers the module with RCT_EXTERN_MODULE.
  • Sources moved into ios/Sources/MixpanelReactNative/ and ios/Sources/MixpanelReactNativeSwift/. The files are unchanged apart from the import below. The podspec source_files and the legacy .xcodeproj now point at the new paths.
  • MixpanelReactNative.swift adds import React under #if SWIFT_PACKAGE, because SPM targets have no bridging header. The CocoaPods build is unchanged.
  • @objc(MixpanelReactNative) keeps the module's runtime name, so the Objective-C registration links to the Swift class under both CocoaPods and SPM.
  • SPM build output (ios/.build, ios/.swiftpm, ios/Package.resolved) is git-ignored and excluded from the npm package.
  • README: new install steps for SPM (npx react-native spm --deintegrate, and spm deinit to undo).

Sample

  • New Samples/MixpanelExampleSpm app on React Native 0.88.0 / React 19.3.0. It has the same screen as MixpanelExample, links iOS through SPM, and is a regular RN 0.88 app on Android.
  • After init(), the sample logs MixpanelExampleSpm ready: native=<true|false> for CI. When the native module is missing, the SDK falls back to JavaScript mode and init() still succeeds, so this line is what shows which mode actually started.

CI

  • test_ios_spm (Xcode 26 and 27) runs npm run spm, then launches a Release build on a simulator. Debug simulator builds skip the JavaScript bundle, so they would launch without running the sample. The job checks the app is still running after 15 s, then checks the simulator log for native=true.
  • test_android_spm launches a Release build on an emulator and checks logcat for native=true.

Testing

Manual

App React Native Platform Linking Result
Samples/MixpanelExample (existing sample) 0.79 iOS CocoaPods ✅ Works
Samples/MixpanelExample (existing sample) 0.79 Android Gradle ✅ Works
Samples/MixpanelExampleSpm (new sample) 0.88 iOS SPM ✅ Works
Samples/MixpanelExampleSpm (new sample) 0.88 Android Gradle ✅ Works
Fresh RN 0.88 app, installing this branch from GitHub (with the Session Replay branch) 0.88 iOS CocoaPods ✅ Works
Fresh RN 0.88 app, installing this branch from GitHub (with the Session Replay branch) 0.88 iOS SPM ✅ Works

Automated

Check Platform Build Result
CI iOS (Xcode 26 and 27), existing sample iOS Debug, CocoaPods ✅ Pass
CI iOS SPM (Xcode 26 and 27), new sample: launch and native=true iOS Release, SPM ✅ Pass
CI test_android, existing sample Android Debug ✅ Pass
CI test_android_spm, new sample: launch and native=true Android Release ✅ Pass
CI test_main_code (JS tests) n/a n/a ✅ Pass
Local run of the CI steps on an iPhone 18 Pro simulator (iOS 27) iOS Release, SPM ✅ native=true
Local run of the CI steps on a Pixel 9a emulator Android Release ✅ native=true

Fixes SDK-105

ketanmixpanel and others added 3 commits October 26, 2026 16:14
Adds ios/Package.swift for React Native 0.88's experimental SwiftPM
autolinking (`swiftpmConfig.name` in package.json). SwiftPM cannot mix
Swift and Objective-C in one target, so the sources move to ios/Sources
and split into MixpanelReactNativeSwift and MixpanelReactNative (the
RCT_EXTERN_MODULE registration). Under SwiftPM the Swift target imports
React directly, since there is no bridging header.

CocoaPods is unchanged apart from the podspec's source path. mixpanel-swift
is pinned at 7.0.0 in both the podspec and Package.swift. SwiftPM build
output is kept out of the npm package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Samples/MixpanelExampleSpm is the MixpanelExample screen on React Native
0.88 with the iOS app linked through SwiftPM (`npm run spm`), so the
package's ios/Package.swift is built and run. ios/Podfile is a placeholder
the React Native CLI needs to recognize the iOS project. MixpanelExample
(CocoaPods) is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds test_ios_spm and test_android_spm, copies of test_ios and
test_android for Samples/MixpanelExampleSpm (React Native 0.88). The iOS
job runs `npm run spm` in place of `pod install`, so the package's
ios/Package.swift is built, then launches the app and checks it is still
running, as test_ios does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ketanmixpanel
ketanmixpanel requested review from a team and tylerjroach October 27, 2026 08:01
@linear-code

linear-code Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

SDK-105

SDK-456

@ketanmixpanel ketanmixpanel changed the title feat(analytics): support Swift Package Manager on React Native 0.88+ feat(analytics): support Swift Package Manager on React Native 0.88+ Oct 27, 2026
@greptile-apps

greptile-apps Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The changes appear safe to merge; no new actionable issues were found.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  App[React Native app] --> Choice{Link with}
  Choice --> CocoaPods
  Choice --> SwiftPM
  CocoaPods --> Sources[Swift and Objective-C sources]
  SwiftPM --> Registration[Objective-C registration target]
  Registration --> Implementation[Swift implementation target]
  Implementation --> SDK[Mixpanel Swift SDK]
Loading

Reviews (4) · Last reviewed commit: "docs(ios): explain why the Objective-C t..." · Reviewed by Greptile

Comment thread .github/workflows/node.js.yml Outdated
Comment thread .github/workflows/node.js.yml
ketanmixpanel and others added 3 commits October 27, 2026 14:54
CI appends org.gradle.jvmargs to this file with `echo >>`. Without a trailing
newline it joined the last line, so android.newDsl failed to parse as a boolean
and test_android_spm could not build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…module

The iOS and Android SPM jobs built Debug, which ships no JavaScript bundle, so
the app launched without running the sample and nothing exercised the SDK.
They now build Release and look for a line the sample logs after init(). It
reports whether the native module is present, since the SDK falls back to
JavaScript mode, where init() still succeeds, when the module is missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…heme

The scheme still listed MixpanelExampleSpmTests from the React Native template,
but the project has no such target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@rahul-mixpanel rahul-mixpanel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the library changes and the moved sources against master. No blocking issues, and CocoaPods users are unaffected.

Checked and looks good: import React under #if SWIFT_PACKAGE is correctly scoped; @objc(MixpanelReactNative) lets RCT_EXTERN_MODULE link to the Swift class; the podspec and legacy .xcodeproj paths are updated; the sample's native= check uses the same NativeModules.MixpanelReactNative lookup as the SDK (index.js:5); and npm pack --dry-run ships Package.swift and Sources/**, leaving out .build, .swiftpm and Package.resolved. Using this together with mixpanel/mixpanel-react-native-session-replay#95 under SwiftPM resolves cleanly, since both pinned SDKs depend on mixpanel-swift-common from: "2.0.0".

Inline comments are mostly nits, plus one versioning decision.

Comment thread ios/Package.swift
],
dependencies: [
// Keep in step with the Mixpanel-swift version in MixpanelReactNative.podspec.
.package(url: "https://github.com/mixpanel/mixpanel-swift.git", exact: "7.0.0"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This matches the podspec, so it's a decision rather than a bug, but it's worth making on purpose. Apps that use mixpanel-swift natively and through React Native are common, and under SwiftPM they can't resolve to two different versions. .upToNextMinor(from: "7.0.0") would avoid that, or the README could document the constraint.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exact is intentional, and it matches the podspec. It means the mixpanel-swift an app builds against is the same version CI built and tested with this wrapper. A range would let a developer's build pick up a release we haven't tested together, and it would push native SDK updates into apps without the developer choosing to upgrade. mixpanel-swift bumps reach apps through a wrapper release.

You're right about the trade-off: an app that also depends on mixpanel-swift directly at another version won't resolve. I've added a README note in bd9a0a4 saying the package pins mixpanel-swift, and that apps using it directly should use the version this release pins.

Comment thread ios/Package.swift Outdated
name: "MixpanelReactNative",
dependencies: ["MixpanelReactNativeSwift"] + reactNativeProducts,
path: "Sources/MixpanelReactNative",
publicHeadersPath: ".",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this target has no headers, so publicHeadersPath can go. linkerSettings on the next line isn't needed either, since UIKit and Foundation are linked automatically, and the session-replay package doesn't set it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped linkerSettings in 9a661cb. publicHeadersPath has to stay. SwiftPM requires an Objective-C target's public headers folder to exist, and it defaults to include/, which this target doesn't have. Without the setting, the manifest fails to load with "public headers ("include") directory path for 'MixpanelReactNative' is invalid or not contained in the target". I reproduced it with a minimal package, just one .m file and no React Native, on Swift 6.3.3 (Xcode 26) and 6.4 (Xcode 27).

An empty include/ would also work, but git doesn't track empty folders and npm wouldn't ship one. So it points at the target's own folder instead, which holds only the .m file and so exports nothing. 212babf updates the comment to explain this.

Comment thread ios/Package.swift
.product(name: "ReactHeaders", package: "ReactNative"),
.product(name: "ReactNativeHeaders", package: "ReactNative"),
.product(name: "ReactNativeDependenciesHeaders", package: "ReactNative"),
.product(name: "ReactAppHeaders", package: "React-GeneratedCode"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: ReactAppHeaders holds the codegen output, but this is a legacy bridge module that doesn't use codegen. It could be dropped from both targets.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping it for now. We plan to convert this module to a TurboModule, which needs the codegen headers, so I'd rather not drop it and add it back.

Comment thread .github/workflows/node.js.yml Outdated
- name: Install dependencies
run: |
npm install
npm install -g react-native-cli

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: copied from test_android. react-native-cli is deprecated, and npm i react-native-gradle-plugin modifies the root package.json. Neither should be needed for a sample that runs npx react-native.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed from the SPM job in a7a2836. I've left test_android as it is for a separate change.

Comment thread .github/workflows/node.js.yml Outdated
path: |
~/.android/avd/*
~/.android/adb*
key: avd-${{ matrix.api-level }}-google-apis-x86_64-nexus5x

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this is the same AVD cache key as test_android, so the two jobs race to save the same entry.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SPM job now has its own key (…-nexus5x-spm), in f805024.

resolver: {
nodeModulesPaths: [
path.resolve(__dirname, 'node_modules'),
path.resolve(__dirname, '../../node_modules'),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not verified, and MixpanelExample already did the same: the repo root's node_modules has react-native ^0.76 from the library's dev dependencies. That could put a second copy of React Native into the sample's bundle. CI passes, so it may be fine, but it's worth a quick look.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed. A Release bundle had 403 modules from the root's react-native 0.76.9 as well as the sample's 0.88. d1dbffe blocks the root's react and react-native, so the library resolves to the sample's copies. The bundle now has a single React Native and went from 1.8 MB to 985 KB. The sample still logs native=true on iOS and Android. MixpanelExample has the same setup, and I've left it for a separate change.

ketanmixpanel and others added 7 commits October 28, 2026 15:40
UIKit and Foundation are linked automatically. The target keeps its public headers
path although it has no headers: without it SwiftPM looks for include/ and fails to
resolve the package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sample runs its own React Native CLI and Gradle plugin through npx, so the
deprecated react-native-cli and the root react-native-gradle-plugin install aren't
needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_android uses the same key and runs at the same time, so the two jobs raced
to save one entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The library resolves to the repo root, whose node_modules has react-native 0.76
and react from the library's dev dependencies, so the bundle held both those and
the sample's 0.88 copies. Block the root copies so the library's imports resolve
to the sample's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ping

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@rahul-mixpanel rahul-mixpanel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the feedback, and for the correction on publicHeadersPath. CI is green. LGTM.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants