I added three mediation adapters in one sitting to nudge my AdMob eCPM a little higher. Across my four iOS apps (Beautiful HD Wallpapers, Ukiyo-e Wallpapers, Relaxing Healing, and Law of Attraction Everyday — my RWD-iOS group), I dropped in the Unity Ads, Liftoff, and InMobi adapters. Builds that had been green for months all failed at once with ld: framework not found. I hadn't touched the AdMob SDK itself.
Mediation is often described as "just add the adapter," but in practice you have to touch both the link step and the Info.plist, or you get a two-stage trap: the build won't compile, or it compiles but the ads never fill. Here's the symptom-by-symptom breakdown I wish I'd had on hand.
Split the symptom into three buckets first
Post-adapter trouble can look the same on the surface while the root cause lives in different layers. Pinning down which symptom you have saves a lot of blind trial and error.
- Symptom A: the build fails at link time —
ld: framework not found GoogleMobileAdsMediation...,Undefined symbols for architecture arm64, or aduplicate symbolerror - Symptom B: it builds, but mediation ads never fill — your AdMob mediation report shows zero requests (or a 100% error rate) for the new network
- Symptom C: it builds and shows ads, but measurement is off — SKAdNetwork postbacks don't arrive and the network's own dashboard reports almost nothing
A is about link and Pod consistency; B and C are almost always Info.plist and initialization order. Let's knock them down in order.
Symptom A: linker errors usually mean a version mismatch
When you see framework not found or Undefined symbols, nine times out of ten the adapter and the AdMob core SDK (Google-Mobile-Ads-SDK) versions don't line up. Each adapter targets a specific range of AdMob SDK versions, and when that drifts, the adapter expects symbols that the core doesn't expose, and linking fails.
Start by pinning both the core and the adapters in your Podfile. If you specify loosely with ~> and let pod update decide, the adapters tend to jump to a newer release on their own and break the match — so I prefer pinning the numbers.
# Podfile — pin the AdMob core and mediation adapters to matching versions
target 'YourApp' do
use_frameworks! :linkage => :static
# AdMob core (match the range your adapters support)
pod 'Google-Mobile-Ads-SDK', '12.4.0'
# Mediation adapters (pick the build that supports core 12.x)
pod 'GoogleMobileAdsMediationUnity'
pod 'GoogleMobileAdsMediationVungle' # Liftoff ships under the Vungle adapter name
pod 'GoogleMobileAdsMediationInMobi'
endThe :linkage => :static line matters. If you've moved Firebase to SPM and kept only the AdMob stack on CocoaPods — a hybrid setup — dynamic frameworks and static libraries can mix and trigger duplicate symbol. Forcing the ad adapters to link statically makes that conflict mostly disappear.
After editing the Podfile, reinstall cleanly so you don't drag along a half-broken state.
# Drop the old Pods and lockfile before reinstalling
rm -rf Pods Podfile.lock
pod deintegrate
pod install --repo-update
# Clear Xcode's build cache too (a stale one re-triggers "framework not found")
rm -rf ~/Library/Developer/Xcode/DerivedData/*If Undefined symbols still won't clear, suspect that the network's own SDK (e.g. InMobiSDK) — not just the adapter — failed to install. Check the pod install log for the core SDK appearing under Installing .... The adapter is only a bridge; the network's underlying SDK comes in as a separate Pod.
Symptom B: it builds but shows nothing — look at Info.plist
Once linking succeeds, the next common one is "mediation ads never come back." If your AdMob mediation report keeps showing zero requests for just the new network, your Info.plist is probably missing entries under SKAdNetworkItems.
On iOS, unless you list each ad network's SKAdNetwork ID in Info.plist, that network's delivery and measurement won't work. Adding the adapter does not insert these IDs automatically.
<!-- Info.plist — list one SKAdNetwork ID per network you added -->
<key>SKAdNetworkItems</key>
<array>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>cstr6suwn9.skadnetwork</string> <!-- Google AdMob -->
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>4dzt52r2t5.skadnetwork</string> <!-- Unity Ads -->
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>gta9lk7p23.skadnetwork</string> <!-- Vungle / Liftoff -->
</dict>
</array>Always pull the exact ID for each network from the official, current list — these IDs change over time. Rather than hand-adding one per network, I start from the consolidated mediation list Google publishes and trim it down to the networks I actually use. If your project is managed by Expo, write them into ios.infoPlist in app.config.js instead of editing Info.plist directly, so they survive prebuild.
// app.config.js — keep SKAdNetworkItems from being wiped by Expo prebuild
export default {
ios: {
infoPlist: {
SKAdNetworkItems: [
{ SKAdNetworkIdentifier: 'cstr6suwn9.skadnetwork' },
{ SKAdNetworkIdentifier: '4dzt52r2t5.skadnetwork' },
{ SKAdNetworkIdentifier: 'gta9lk7p23.skadnetwork' },
],
},
},
};Symptom C: init order and ATT can wreck measurement
If everything builds and shows ads but a network's revenue is far below reality, suspect the order of the ATT (App Tracking Transparency) prompt and your ad SDK initialization. If you initialize MobileAds before calling requestTrackingAuthorization, the SDK starts in a not-authorized state, and some networks keep sending lower-accuracy requests.
The order should be: request ATT right after launch, and only initialize the ad SDK once you have the response.
// At launch: wait for the ATT response, then initialize MobileAds
import AppTrackingTransparency
import GoogleMobileAds
func initializeAdsAfterATT() {
ATTrackingManager.requestTrackingAuthorization { _ in
// Initialize once the response is settled — authorized or not
DispatchQueue.main.async {
MobileAds.shared.start(completionHandler: nil)
}
}
}Note that you should not skip initialization just because the user denied tracking — ads still serve when denied. The goal is only to initialize after the ATT response is settled. If you control ads from the React Native side, confirm the native launch sequence still honors this order around AppDelegate.
Prevention: add adapters one at a time
My biggest takeaway this round was that I added all three adapters at once. When the linker error hit, isolating which adapter caused it cost me extra time. After running a group of apps with over 50 million cumulative downloads for twelve years, I keep relearning that for revenue-related changes, "one at a time, confirm the build is green, then move on" is the fastest path overall.
Concretely: add one adapter → confirm a clean build → confirm a test ad fills on a real device → confirm requests appear in the AdMob console — finish that for one network before moving to the next. Add the SKAdNetworkItems entries one network at a time too, so you can narrow the cause if measurement breaks.
One more thing: always register an AdMob test device ID and verify with test ads. Repeatedly clicking live ads is a policy violation and undoes the configuration you just got working.
Reference links
Once it's configured, mediation is a low-maintenance system — but that first integration stacks several layers and is easy to get stuck in. I hope this helps you isolate where yours is stuck.