fix(ios): Swift header not found under Xcode 26 explicit modules - #278
Conversation
Greptile SummaryThis PR conditionally prefers the module-qualified generated Swift header to support Xcode 26 explicit-module dependency scanning while preserving the existing quoted import as a fallback.
|
| Filename | Overview |
|---|---|
| packages/purchasely/ios/PurchaselyRN.m | Safely adds a guarded module-qualified Swift header import without changing the fallback behavior of existing build configurations. |
Reviews (1): Last reviewed commit: "fix(ios): Swift header not found under X..." | Re-trigger Greptile
|
Heads up: this fix unblocks a stuck batch of 7 open Dependabot PRs (#271–#277) — every one of them fails Generated by Claude Code |
## Summary
`build-ios` has failed on **every** PR since late July — including PRs
that only touch a test-project lockfile. The failure is the CI cache,
not the app code.
```
"typeinfo for facebook::react::YogaStylableProps", referenced from ...
"vtable for facebook::react::DebugStringConvertible", referenced from
... in libRNScreens.a(RNSFullWindowOverlay.o)
ld: symbol(s) not found for architecture arm64
```
## Root cause
The CocoaPods cache included `example/ios/Pods` — the **generated**
install tree — behind a loose `restore-keys: ${{ runner.os
}}-cocoapods-` fallback. The v6 migration (#243, merged 2026-07-24)
changed the Podfile/podspec hash, so the exact key has never matched
since. Every run therefore restores a **pre-v6 Pods tree**. From the
failing run's log:
```
key: macOS-cocoapods-21c48bdc... (requested)
restored: macOS-cocoapods-a2f4547b... (stale, different key)
```
`pod install` does not fully reconcile a stale tree. RNScreens then
compiled against **stale React Native headers** while linking the
**current** prebuilt `React.xcframework` — a header/binary mismatch that
surfaces as those missing `facebook::react::*` vtables.
The timeline matches exactly: `build-ios` passed through 2026-07-25 and
has failed on every run since, across unrelated branches.
## Verification
On this same commit, with a clean `pod install` and the exact CI
`xcodebuild` invocation:
```
** BUILD SUCCEEDED **
```
Same prebuilt core, same Xcode 26. **Only the stale cache distinguishes
CI from a working build.** As a control, a *stale* local Pods tree
failed too — with a different stale-artifact error (`Build input file
cannot be found: RCTConvert_PurchaselyRN.m`, a file the v6 reorg
removed), independently showing that a stale Pods tree survives `pod
install` and breaks the build.
## Fix
- **Cache only `~/.cocoapods` and `~/Library/Caches/CocoaPods`.** These
are content-addressed download caches, so a restore-key hit is safe and
still avoids re-downloading pods. The Pods tree is regenerated each run
(~20s locally).
- **Add `yarn.lock` to both cache keys** so a React Native version bump
invalidates them.
- **Drop the DerivedData `restore-keys`** so it is exact-match only. A
partial hit would mix object files built against a different Pods
install — the same staleness class of bug. It was latent here (that
cache was cold in the failing run), but it is the same footgun.
Applied to both jobs that install the example Pods: `build-ios` and
`ios-unit-tests`.
## Note on what this does *not* change
No app, Podfile, or dependency changes — the diff is workflow-only. That
is deliberate: if `build-ios` goes green here, it confirms the
diagnosis, since nothing but the cache behaviour changed.
Unrelated and left alone: #278 fixes a *different* iOS problem (a Swift
header **compile** error under Xcode 26 explicit modules); it does not
address this **link** failure.
## Test plan
- [x] Clean `pod install` + CI's exact `xcodebuild` command locally —
`** BUILD SUCCEEDED **`
- [x] Confirmed the failing CI run restored a stale cache under a
different key
- [x] Confirmed DerivedData cache was cold in the failing run (so Pods
is the culprit)
- [x] `ruby -ryaml` parses the workflow; all 7 jobs still present
- [ ] `build-ios` green in CI on this PR (the real test)
- [ ] `iOS Unit Tests (bridge)` still green with the narrowed cache
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
a0b2cb3 to
38d24c8
Compare
Add conditional import for Swift header based on availability.
Problem
Building an app that depends on
react-native-purchasely(6.0.0) under Xcode 26, with the default "Explicitly Built Modules" build setting, fails with:pointing at
PurchaselyRN.m's#import "react_native_purchasely-Swift.h".Root cause
PurchaselyRN.mmixes Objective-C and Swift sources in the same CocoaPods target (it imports the compiler-generated Swift interface header to usePLYTransitionFactory, defined inPLYTransitionFactory.swift). The import is a plain, quoted#import "...".Under Xcode 26's explicit module builds, Clang's dependency scanner doesn't treat a quoted, non-modular
#importas an edge to the pod's own module — so it has no guarantee that this target's Swift compilation (which emits the generated header) has run before the ObjC file is scanned/compiled. The header sometimes doesn't exist yet when the ObjC file needs it.Fix
Prefer the angle-bracket, module-qualified form of the import (
<react_native_purchasely/react_native_purchasely-Swift.h>), guarded with__has_includeso it falls back to the original quoted import wherever the module path isn't available (older Xcode, non-explicit-modules builds, etc.):The module-qualified form lets Clang's explicit-module scanner resolve it as a real dependency on the
react_native_purchaselymodule, so the Swift compile is correctly ordered before this file.Testing
Verified with a real
bun run ios(Expo / React Native 0.85, Xcode 26) build, install, and app launch on iOS Simulator — fails with the exact error above without this change, succeeds with it. No Podfile-level workaround (e.g. disablingSWIFT_ENABLE_EXPLICIT_MODULES/CLANG_ENABLE_EXPLICIT_MODULESfor the pod target) was needed once this import is fixed.