Skip to content

fix(ios): configure the Swift AppDelegate on Expo SDK 53+ - #32

Merged
GSTJ merged 2 commits into
masterfrom
fix/ios-swift-app-delegate
Jul 27, 2026
Merged

fix(ios): configure the Swift AppDelegate on Expo SDK 53+#32
GSTJ merged 2 commits into
masterfrom
fix/ios-swift-app-delegate

Conversation

@GSTJ

@GSTJ GSTJ commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Closes #30, which has the diagnosis.

Silent no-ops

This is why the Swift breakage below lasted five SDK releases, so it gets its own commit.

replaceIfNotFound and addBelowAnchorIfNotFound both ended up calling String.replace with a needle that wasn't in the file, which hands back the input untouched. A mod that matched none of its anchors wrote nothing and reported success.

addBelowAnchorIfNotFound did have a throw, sitting behind the wrong check: it ran the replace first and only tested the anchor when the insertion was already present, so the one case worth catching fell through it. Both helpers now check for the change already being applied, then throw, then edit. Android guarded every call site with its own includes test, so nothing moves there.

Swift

SDK 53 is where the template changed. The issue says 52; I corrected it there too. I pulled expo-template-bare-minimum for every release from 50 to 57: AppDelegate.mm through 52, AppDelegate.swift from 53 on. The ReactNativeDelegate.bundleURL() block is byte-identical across 53 to 57, so one anchor covers all five.

The release arm of that method becomes CodePush.bundleURL(). The #if DEBUG arm keeps loading from Metro, which CodePush has nothing to serve. Matching is a pattern, so a reflowed call still lands.

Swift can't see CodePush directly. It's an Objective-C pod with no module map, so the import goes in the bridging header, and the path comes from the Xcode project's SWIFT_OBJC_BRIDGING_HEADER. Objective-C app delegates import CodePush themselves and skip that step.

Proof

Blank create-expo-app on expo@57.0.8, plugin in app.json, npx expo prebuild --platform ios --clean.

Before, v1.0.12 from npm. Prebuild says it finished, grep -rl CodePush ios/ finds only Info.plist:

prebuild reporting success on SDK 57 while AppDelegate.swift and the bridging header stay untouched

After, this branch packed into the same app. Running prebuild twice without --clean leaves one of each:

prebuild redirecting bundleURL to CodePush and adding the import to the bridging header

The prebuilt app compiles. pod install, then a Release simulator build with signing off, which is the configuration the fix touches. 271 CodePush symbols land in the app binary. Deleting only the bridging-header import from that same tree fails at AppDelegate.swift:66 with cannot find 'CodePush' in scope:

xcodebuild succeeding, then failing when the bridging-header import is removed

expo@50.0.21 on the same branch, Objective-C output unchanged and the bridging header left alone:

prebuild on SDK 50 importing CodePush in AppDelegate.mm and skipping the bridging header

Local checks:

lint, format, typecheck, 66 tests, build and pack all passing

Tests run against the SDK 50, 52, 53 and 57 templates verbatim, kept in src/ios/__tests__/templates.ts, and cover both throw paths on each language. 66 tests, up from 31.

Out of scope

The SDK 50 app got no Xcode build, only prebuild. Its AppDelegate.mm and bridging header come out byte-identical to what published v1.0.12 writes, which I diffed.

The simulator build proves the generated code compiles and links. It doesn't prove CodePush serves an update at runtime, which needs a server and a signed release build, and react-native-code-push@9.0.1 hasn't shipped since the repo was archived.

https://claude.ai/code/session_01QsVEsfkynXpJWK4t33exG9

GSTJ added 2 commits July 27, 2026 20:46
`replaceIfNotFound` and `addBelowAnchorIfNotFound` both ended up calling
`String.replace` with a needle that wasn't there, which returns the input
untouched, so a mod that found none of its anchors reported success and
wrote nothing. That is how #30 sat unnoticed across five SDK releases
while the Android side, which throws, got reported the week SDK 57
landed.

`addBelowAnchorIfNotFound` did have a throw, but behind the wrong check:
it ran the replace first and only tested the anchor when the insertion
was already present, so the one case worth catching fell through. Both
now check for the change being already applied, then throw on a missing
anchor, then edit.

Android already guarded every call with its own `includes` check, so
nothing changes there.

Claude-Session: https://claude.ai/code/session_01QsVEsfkynXpJWK4t33exG9
The mod looked for `#import "AppDelegate.h"` and the Objective-C
`[[NSBundle mainBundle] URLForResource:@"main" ...]` line. SDK 53
rewrote the template in Swift, so from 53 through 57 neither anchor is
in the file and the app kept booting from the bundle baked into the IPA.

Swift gets the release arm of `ReactNativeDelegate.bundleURL()` swapped
for `CodePush.bundleURL()`. The `#if DEBUG` arm stays on Metro. That
block is byte-identical across 53 to 57, and the pattern tolerates the
call being reflowed.

Swift cannot import CodePush directly, since it is an Objective-C pod
with no module map, so `#import <CodePush/CodePush.h>` goes into the
bridging header instead. Its path comes from the Xcode project's
`SWIFT_OBJC_BRIDGING_HEADER` rather than from guessing at the project
name. Objective-C app delegates import CodePush themselves and skip it.

SDK 50 through 52 keep the Objective-C path unchanged.

Closes #30

Claude-Session: https://claude.ai/code/session_01QsVEsfkynXpJWK4t33exG9
@GSTJ
GSTJ merged commit bb07d7b into master Jul 27, 2026
4 checks passed
@GSTJ
GSTJ deleted the fix/ios-swift-app-delegate branch July 27, 2026 23:56
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.

iOS AppDelegate mod does nothing on Expo SDK 53+ and does not say so

1 participant