Skip to content

refactor(ios): move RoktContracts-typed code into a Swift layer - #425

Open
thomson-t wants to merge 2 commits into
thomson-t/spm-04-expo-spmfrom
thomson-t/spm-05-swift-rokt-bridge
Open

thomson-t wants to merge 2 commits into
thomson-t/spm-04-expo-spmfrom
thomson-t/spm-05-swift-rokt-bridge

Conversation

@thomson-t

@thomson-t thomson-t commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Why

React Native is moving apps to Swift Package Manager, and in that setup this package's Objective-C++ files cannot see the Rokt types they use today (the embedded view, events and config). The next pull request, an experimental package manifest for React Native's own Swift Package Manager mode, needs that code to live somewhere that can see them. Once this lands, all Rokt-typed logic is in Swift, and the Objective-C++ files only pass plain views and dictionaries through. Apps see no difference: the same events reach JavaScript, with the same contents, in the same order.

Programme

Part of the plan to support Swift Package Manager in this package before the CocoaPods central repository becomes read-only on 2026-12-02 (CocoaPods announcement). This is the fifth of eight pull requests, all merging into the workstation/spm-migration branch, which merges into main once the series is complete, and shipping together in one release; the plan record is internal and cannot be linked here.

What changes

Before: RoktEventManager.mm mapped every Rokt event type to its JavaScript payload, RNMPRokt.mm built the Rokt config and checked for embedded views, and the Fabric view created a RoktEmbeddedView directly. All of them imported the RoktContracts headers.

After: that logic is in three Swift classes in ios/RNMParticle/Swift/:

  • RNMPRoktEventMapper: event to payload, plus the callback and height side events;
  • RNMPRoktConfigFactory: dictionary to config;
  • RNMPRoktViews: creates and recognises the embedded view.

The Objective-C++ files reach them through one hand-written header, RNMPRoktSwift.h, which uses only Foundation and UIKit types; the generated Swift header can't be reached from Objective-C++ under Swift Package Manager. No .mm file references RoktContracts any more, and the import header now gives RoktContracts to .m files only. The Fabric view's roktEmbeddedView property is typed UIView. The podspec gains swift_version, DEFINES_MODULE, private headers (so C++ and React headers stay out of the module), and, in Swift Package Manager mode, the Swift include path for the packages.

The tests were written first, against the original Objective-C, and pass unchanged against the Swift port:

  • one test per Rokt event type, covering the payload and the extra events it triggers, in order;
  • the config mapping, including which inputs mean "no config";
  • a check that every selector declared in the hand-written header exists.

CI runs the three new test classes in both legs. No documentation change is needed; the changelog is generated by the release-draft workflow.

This pull request also fixes a crash that predates the series (3.4.0 has it). The Fabric view kept the plain ViewProps its superclass starts with, so debug builds of React Native threw NSInternalInconsistencyException ("RCTViewComponentView subclasses (and RoktNativeLayoutComponentView particularly) must setup _props instance variable with a default value in the constructor.") as soon as a RoktLayoutView mounted. Release builds of React Native only log that check. initWithFrame: now starts from RoktNativeLayoutShadowNode::defaultSharedProps(), as the superclass does with ViewShadowNode::defaultSharedProps(), and a new test checks the view's initial props type. The fix is its own commit on this branch.

Start reading at RNMPRoktSwift.h, then RoktEventManager.mm (the event adapter), then the Swift files.

Linked work

Depends on: the Expo config plugin option (branch thomson-t/spm-04-expo-spm) and the pull requests below it in this series; merge those first.
Unblocks: the experimental Package.swift for React Native's Swift Package Manager mode (branch thomson-t/spm-06-package-swift).

Rollout

Path: this merges into workstation/spm-migration, not main, so nothing reaches main or a release until the whole series has merged there and that branch is merged into main. It then ships in the next release. It is live for every iOS app on that release.
Feature flags: none.
Turning it off: a published version cannot be recalled. Reverting this pull request and releasing again restores the Objective-C mapping in the next version.
What we watch: this repository's issues, for Rokt events missing or changed in JavaScript, embedded placements that stop resizing, and iOS build errors mentioning Swift.

Risks

  • An event could reach JavaScript with a different payload; prevented because characterization tests for every event type were written against the Objective-C and pass against Swift, and the sample app showed the full event sequence and a resizing embedded view; we would see a missing or renamed field in an app's event handler.
  • The hand-written header could drift from the Swift code; prevented because a test checks every declared selector exists; we would see an "unrecognized selector" crash.
  • Apps that use static libraries without the pre_install dynamic-framework hook fail pod install ("Swift pod react-native-mparticle depends upon mParticle-Apple-SDK-ObjC, which does not define modules"); contained because the native mParticle-Apple-SDK pod already fails the same way in that setup, so the hook, or use_frameworks!, is already required; we would see that error reported against that release by an app that had worked before.
  • Private headers could hide something an app imported from this pod; contained because React Native finds this package's modules and view by class name, and the sample, Expo and Swift Package Manager builds all passed; we would see a header-not-found error in an app.
  • If any cacheAttributes value is not a string, the config factory drops the whole cacheAttributes map, where the Objective-C passed such values through unchecked (and they would have crashed when read); not addressed further, because the JavaScript type allows only strings; we would see cache attributes ignored.

Risk class: higher — this rewrites how every Rokt event and config reaches the SDK on iOS. Behaviour is pinned by tests written against the old code.

Who

Written by: an automated coding agent (Claude Code), at an engineer's request, following the internal Swift Package Manager migration plan and its proof of concept.
Code reviewed before opening: an independent review agent reviewed the change before it was committed.
Design reviewed before opening: the requesting engineer approved the plan's Swift-first layering, with hand-written Objective-C headers for the Swift classes.
Decision this implements: the requesting engineer's approval of the migration plan on 2026-09-28; the record is internal and cannot be linked.
Checked: on 2026-09-28, with Xcode 27.0 on an iOS 26.5 simulator, the sample app (React Native 0.84, New Architecture):

  • Tests: the characterization tests passed against the Objective-C before the port, and all 54 unit tests pass after it, in both CocoaPods and Swift Package Manager mode.
  • CocoaPods mode, and Swift Package Manager mode with static libraries and with dynamic frameworks: Release build and unsigned archive succeeded; one copy of each SDK class; embedded placements by name and by tag, and an overlay, rendered, with the events shown on screen and the embedded view resizing.
  • Size: the archived app binary grew by 25.4 KB in CocoaPods mode and 0.8 KB in Swift Package Manager mode.
  • Old Architecture: a React Native 0.81.5 app with the New Architecture off compiled this package; that app needed a local fix for React Native's own fmt dependency, which does not build with Xcode 27.
  • Props fix, on 2026-10-01 (Xcode 27.0, iOS 27 simulator): in the sample app in Debug, Swift Package Manager mode, the new props test failed without the fix and passed with it, alongside the existing sizing tests. A fresh React Native 0.87.1 app in React Native's own Swift Package Manager mode, in Debug, threw this exception as soon as its RoktLayoutView mounted; with the fix it mounts the view and keeps running.
    Not checked: the Old Architecture at runtime; tvOS. CI, which builds with Xcode 16, passed on this pull request.

Size

Hand-written: about 535 lines added and 210 removed in 17 files; 303 of the added lines are tests, and the Swift and adapter code is about 230.
Generated: none.
Why one pull request: the tests pin the old behaviour and must land with the port they guard, and the adapters cannot switch to Swift one file at a time without leaving an Objective-C++ file that still needs the Rokt headers. The props fix rides along because this is the pull request in the series that changes the Fabric view.

Notes for reviewers

Why hand-written headers. Under Swift Package Manager, Swift and Objective-C++ must be separate targets, and an Objective-C++ file can only reach another target's generated -Swift.h with C++ modules, which break React Native's headers. The mParticle SDK's MPRokt.h only forward-declares RoktEmbeddedView, RoktConfig and RoktEvent, so pointers to them still type-check in .mm files without the RoktContracts headers.

The characterization test's spy. RNMPRoktEventMapperTests swaps the event manager's class for a subclass that records sendEventWithName:body: instead of sending. The subclass adds no instance variables, so swapping the class of an existing instance is safe.

A clamp worth knowing. CacheConfig raises a zero duration to its 90-minute maximum. A config test now pins that, since the JavaScript wrapper sends 0 when the duration is missing.

🤖 Generated with Claude Code

@thomson-t
thomson-t added this pull request to stack #427 September 29, 2026 21:25
@thomson-t
thomson-t marked this pull request as ready for review September 30, 2026 13:19
@thomson-t
thomson-t requested a review from a team as a code owner September 30, 2026 13:19
Copilot AI balanced review requested due to automatic review settings September 30, 2026 13:19
@cursor

cursor Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

PR Summary

Medium Risk
Refactors iOS Rokt event handling and config mapping across language boundaries and modifies build configurations, but functional behavior is covered by comprehensive unit tests.

Overview
Refactors iOS Rokt integration logic out of Objective-C++ into a dedicated Swift layer (RNMPRoktEventMapper, RNMPRoktConfigFactory, and RNMPRoktViews) bridged via RNMPRoktSwift.h. This isolates Rokt contracts from C++ translation units in preparation for Swift Package Manager support.

Updates react-native-mparticle.podspec and Xcode build settings to support Swift compilation and private modular headers. In addition, initializes _props with RoktNativeLayoutShadowNode::defaultSharedProps() in RoktNativeLayoutComponentView to resolve Fabric debug assertion failures.

Adds native unit test suites covering Rokt event mapping, configuration generation, and Swift bridging selector availability, and enables them in CI workflows.

Reviewed by Cursor Bugbot for commit 3b0b440. Bugbot is set up for automated code reviews on this repo. Configure here.

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

The checked-in RNMParticle Xcode target does not compile the new Swift implementations referenced by its Objective-C++ sources.

Review effort: Balanced
Findings: 1 High severity

Open (1)
What changed in this PR

Moves Rokt-typed iOS logic into Swift to support future Swift Package Manager integration while preserving the Objective-C++ bridge.

Changes:

  • Adds Swift event, configuration, and embedded-view adapters.
  • Updates Objective-C++ callers and CocoaPods settings.
  • Adds characterization tests to both CI modes.
File Description
.github/​workflows/​pull-request.yml Runs new iOS tests.
react-native-mparticle.podspec Configures Swift and module support.
ios/​RNMParticle/​RNMPRoktSwift.h Declares the Swift bridge API.
ios/​RNMParticle/​RNMPSDKImports.h Restricts RoktContracts imports to Objective-C.
ios/​RNMParticle/​RNMPRokt.mm Uses Swift config and view adapters.
ios/​RNMParticle/​RoktEventManager.mm Delegates event mapping to Swift.
ios/​RNMParticle/​RoktNativeLayoutComponentView.h Exposes the embedded view as UIView.
ios/​RNMParticle/​RoktNativeLayoutComponentView.mm Creates embedded views through Swift.
ios/​RNMParticle/​Swift/​RNMPRoktEventMapper.swift Maps Rokt events and side effects.
ios/​RNMParticle/​Swift/​RNMPRoktConfigFactory.swift Builds Rokt configuration objects.
ios/​RNMParticle/​Swift/​RNMPRoktViews.swift Creates and identifies embedded views.
ios/​RNMParticle.xcodeproj/​project.pbxproj Adds the bridge header, but omits Swift sources.
sample/​ios/​MParticleSampleTests/​RNMPRoktEventMapperTests.m Characterizes event payloads and ordering.
sample/​ios/​MParticleSampleTests/​RNMPRoktConfigFactoryTests.m Tests configuration conversion.
sample/​ios/​MParticleSampleTests/​RNMPRoktSwiftTests.m Verifies bridge selectors and types.
sample/​ios/​MParticleSample.xcodeproj/​project.pbxproj Registers the new tests.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread ios/RNMParticle.xcodeproj/project.pbxproj
@thomson-t
thomson-t force-pushed the thomson-t/spm-05-swift-rokt-bridge branch 2 times, most recently from 0a31cef to 78250f6 Compare October 2, 2026 14:59
@thomson-t
thomson-t force-pushed the thomson-t/spm-05-swift-rokt-bridge branch from 78250f6 to 8d92742 Compare October 5, 2026 18:06
@thomson-t
thomson-t dismissed nickolas-dimitrakas’s stale review October 5, 2026 19:35

The merge-base changed after approval.

@thomson-t
thomson-t force-pushed the thomson-t/spm-05-swift-rokt-bridge branch from 8d92742 to 6c464f0 Compare October 5, 2026 19:35

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

# The Rokt-typed code is Swift (ios/RNMParticle/Swift); Objective-C++ reaches it through the
# hand-written RNMPRoktSwift.h. Private headers keep C++ and React headers out of the module's
# umbrella header, which the Swift code's module would otherwise fail to build.
s.swift_version = '5.0'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Declared swift_version too low for code features

Low Severity

The podspec declares swift_version = '5.0', but the Swift files use the optional binding shorthand (guard let map, if let viewName, if let callback, if let height, let placement) introduced in SE-0345, which requires Swift 5.7. CocoaPods uses this value for compatibility checking, so it would incorrectly allow installation on Xcode 13 (Swift 5.5/5.6), where the code would fail to compile. The value needs to be '5.7' or higher.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6c464f0. Configure here.

thomson-t and others added 2 commits October 5, 2026 21:45
Under Swift Package Manager, Objective-C++ files cannot reach another
target's generated Swift header, so they cannot use the RoktContracts
types. Event mapping (RNMPRoktEventMapper), config building
(RNMPRoktConfigFactory) and the embedded view (RNMPRoktViews) are now Swift.
The .mm adapters reach them through the hand-written RNMPRoktSwift.h, which
uses only Foundation and UIKit types, and no .mm file imports RoktContracts.

The podspec adds swift_version, DEFINES_MODULE and private headers, plus
the Swift include path for packages in Swift Package Manager mode.

Characterization tests for every Rokt event type and for the config
mapping were written against the Objective-C first, and pass unchanged
against the Swift. A drift test checks every selector the header declares.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
RCTViewComponentView's initWithFrame: leaves plain ViewProps in _props.
In a debug build of React Native, the first updateProps asserts that a
subclass has replaced them, so the app crashes with
NSInternalInconsistencyException ("`RCTViewComponentView` subclasses
(and `RoktNativeLayoutComponentView` particularly) must setup `_props`
instance variable with a default value in the constructor.") as soon as
a RoktLayoutView mounts. Release builds of React Native only log the
failed assert, so release builds, and any app that links the
release-flavoured prebuilt React Native frameworks, do not crash.

Start from RoktNativeLayoutShadowNode::defaultSharedProps() in
initWithFrame:, the same way the superclass starts from
ViewShadowNode::defaultSharedProps(), and add a test that the view's
initial props are RoktNativeLayoutProps. The test checks the
props type directly, so it fails whichever React Native flavour the
sample links.

The omission predates this series: 3.4.0 has it too.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@thomson-t
thomson-t force-pushed the thomson-t/spm-05-swift-rokt-bridge branch from 6c464f0 to 3b0b440 Compare October 6, 2026 01:45

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3b0b440. Configure here.

-only-testing:MParticleSampleTests/RNMParticleDuplicateSDKTests \
-only-testing:MParticleSampleTests/RNMPRoktEventMapperTests \
-only-testing:MParticleSampleTests/RNMPRoktConfigFactoryTests \
-only-testing:MParticleSampleTests/RNMPRoktSwiftTests \

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

New props test class missing from CI test list

Medium Severity

The new RoktNativeLayoutComponentViewPropsTests class, which validates the debug-crash props fix, is never run in CI. It lives in RoktNativeLayoutComponentViewSizingTests.mm but is a separate XCTestCase subclass. Because xcodebuild -only-testing: opts in by class name, only RoktNativeLayoutComponentViewSizingTests runs — RoktNativeLayoutComponentViewPropsTests is silently skipped. A future regression in the _props initialization would not be caught.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3b0b440. Configure here.

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.

3 participants