From 3cd997bca7ce15080e056c8dc0e4a2a55f56e577 Mon Sep 17 00:00:00 2001 From: Thomson Thomas Date: Tue, 29 Sep 2026 00:07:04 -0400 Subject: [PATCH] feat(ios): add experimental Package.swift for React Native SPM mode React Native 0.87's Swift Package Manager mode stops with "Package.swift is missing" for this library, and `spm scaffold` drops its native dependencies. This hand-written manifest has two targets, since SwiftPM cannot mix languages in one: the Swift layer, and the Objective-C++ module with RCT_NEW_ARCH_ENABLED defined. The mParticle and RoktContracts version ranges match the podspec, and the package URLs match the kits'. The manifest is shipped in the npm package. A jest test keeps its source list in step with ios/RNMParticle, because CI does not build it. The README describes the mode as experimental and not for production. Co-Authored-By: Claude Opus 5.5 (1M context) --- Package.swift | 73 ++++++++++++++++++++++ README.md | 11 ++++ js/__tests__/package-swift-sources.test.ts | 24 +++++++ package.json | 3 +- 4 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 Package.swift create mode 100644 js/__tests__/package-swift-sources.test.ts diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..e1a12a2 --- /dev/null +++ b/Package.swift @@ -0,0 +1,73 @@ +// swift-tools-version: 6.0 +// Experimental: for React Native's Swift Package Manager mode (React Native 0.87 or later), which +// React Native itself marks as not for production. CocoaPods apps do not use this file. +// +// The ReactNative and React-GeneratedCode packages are the local packages React Native's autolinker +// generates for each app, at these paths relative to this package, the same layout other React +// Native libraries use. Revisit them when React Native publishes a remote Swift package. + +import PackageDescription + +let package = Package( + name: "ReactNativeMparticle", + // React Native's autolinker requires iOS 15; the mParticle SDK declares the same. + platforms: [.iOS(.v15)], + products: [ + .library(name: "ReactNativeMparticle", targets: ["ReactNativeMparticle"]), + ], + dependencies: [ + .package(name: "ReactNative", path: "../../../../xcframeworks"), + .package(name: "React-GeneratedCode", path: "../../../ios"), + // Same URLs as the mParticle kits use, so SwiftPM unifies them into one package identity. + .package(url: "https://github.com/mParticle/mparticle-apple-sdk", "9.2.2"..<"10.0.0"), + .package(url: "https://github.com/ROKT/rokt-contracts-apple.git", "2.0.0"..<"3.0.0"), + ], + targets: [ + // A separate target: SwiftPM cannot mix Swift and Objective-C++ in one target. + .target( + name: "RNMParticleSwift", + dependencies: [.product(name: "RoktContracts", package: "rokt-contracts-apple")], + path: "ios/RNMParticle/Swift" + ), + .target( + name: "ReactNativeMparticle", + dependencies: [ + "RNMParticleSwift", + .product(name: "ReactHeaders", package: "ReactNative"), + .product(name: "ReactNativeHeaders", package: "ReactNative"), + .product(name: "ReactNativeDependenciesHeaders", package: "ReactNative"), + .product(name: "ReactAppHeaders", package: "React-GeneratedCode"), + .product(name: "mParticle-Apple-SDK", package: "mparticle-apple-sdk"), + .product(name: "RoktContracts", package: "rokt-contracts-apple"), + ], + path: ".", + sources: [ + "ios/RNMParticle/RNMPRokt.h", + "ios/RNMParticle/RNMPRokt.mm", + "ios/RNMParticle/RNMPRoktSwift.h", + "ios/RNMParticle/RNMPSDKImports.h", + "ios/RNMParticle/RNMParticle.h", + "ios/RNMParticle/RNMParticle.mm", + "ios/RNMParticle/RoktEventManager.h", + "ios/RNMParticle/RoktEventManager.mm", + "ios/RNMParticle/RoktLayoutManager.m", + "ios/RNMParticle/RoktNativeLayoutComponentView.h", + "ios/RNMParticle/RoktNativeLayoutComponentView.mm", + "ios/RNMParticle/RoktPlaceholderRegistry.h", + "ios/RNMParticle/RoktPlaceholderRegistry.m", + ], + publicHeadersPath: "ios", + // Without RCT_NEW_ARCH_ENABLED the TurboModule and Fabric code compiles out, and the app + // crashes looking up the RoktNativeLayout component. + cSettings: [.define("RCT_NEW_ARCH_ENABLED", to: "1"), .headerSearchPath("ios/RNMParticle")], + cxxSettings: [ + .define("RCT_NEW_ARCH_ENABLED", to: "1"), + .headerSearchPath("ios/RNMParticle"), + .define("DEBUG", .when(configuration: .debug)), + .define("NDEBUG", .when(configuration: .release)), + ], + linkerSettings: [.linkedFramework("UIKit"), .linkedFramework("Foundation"), .linkedFramework("CoreGraphics")] + ), + ], + cxxLanguageStandard: .cxx20 +) diff --git a/README.md b/README.md index a71d923..aec8be5 100644 --- a/README.md +++ b/README.md @@ -419,6 +419,17 @@ Every mParticle and Rokt SDK must then come from Swift Package Manager, and none - The build fails with `[mParticle] $RNMParticleUseSPM is set but the mParticle-Apple-SDK Swift package is not linked into the app target`: `mparticle_spm_post_install` did not run, or the package was removed from the app target afterwards. Call it in `post_install` and run `pod install` again. - A Debug build shows the red box `[mParticle] The mParticle SDK is loaded more than once`: the SDK comes from both CocoaPods and Swift Package Manager. Remove the mParticle and Rokt pods and Swift packages you added by hand, then set up one mode as described above. +### Experimental: React Native Swift Package Manager mode + +> **Not for production.** React Native's own Swift Package Manager mode (React Native 0.87 or later) is experimental, and so is this package's support for it. Use CocoaPods, with or without the opt-in mode above, for apps you ship. + +This package ships a `Package.swift`, so `npx react-native spm add` links it without a scaffolded manifest. The app target must also link the mParticle core SDK and each kit as Swift packages, for example in Xcode (File › Add Package Dependencies): + +- `https://github.com/mParticle/mparticle-apple-sdk`, product `mParticle-Apple-SDK` +- `https://github.com/mparticle-integrations/mp-apple-integration-rokt`, product `mParticle-Rokt` + +Use exactly these URLs, with no `.git` suffix on the first, so Swift Package Manager treats them as the same packages the kits depend on. Then start mParticle in your Swift `AppDelegate` with `import mParticle_Apple_SDK`, as in the CocoaPods setup. + ## Android (Manual Setup) 1. Copy your mParticle key and secret from [your workspace's dashboard](https://app.mparticle.com/setup/inputs/apps) and construct an `MParticleOptions` object. diff --git a/js/__tests__/package-swift-sources.test.ts b/js/__tests__/package-swift-sources.test.ts new file mode 100644 index 0000000..657a6b8 --- /dev/null +++ b/js/__tests__/package-swift-sources.test.ts @@ -0,0 +1,24 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * The podspec compiles every file under ios/ by glob, but Package.swift lists the Objective-C++ + * target's sources by hand, and CI does not build that manifest. Keep the two in step. + */ +const root = path.join(__dirname, '..', '..'); + +it('lists every ios/RNMParticle source in Package.swift', () => { + const manifest = fs.readFileSync(path.join(root, 'Package.swift'), 'utf-8'); + const listed = new Set( + [...manifest.matchAll(/"(ios\/RNMParticle\/[^"]+\.(?:h|m|mm))"/g)].map( + match => match[1] + ) + ); + const onDisk = fs + .readdirSync(path.join(root, 'ios', 'RNMParticle'), { recursive: true }) + .map(String) + .filter(file => /\.(h|m|mm)$/.test(file)) + .map(file => `ios/RNMParticle/${file}`); + + expect([...listed].sort()).toEqual(onDisk.sort()); +}); diff --git a/package.json b/package.json index 4256faa..f9b6fb3 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "lib", "react-native-mparticle.podspec", "app.plugin.js", - "plugin" + "plugin", + "Package.swift" ], "publishConfig": { "access": "public",