A deliberately tiny iOS app for experimenting with Google Ad Manager (GAM) inventory. The UI is an Instagram-style feed of posts with banner / MREC ads inserted between organic content. Every ad has an "Open Ad Inspector" button so you can audit exactly what GAM served, in real time, on device.
The goal is experimentation, not production polish. Code is structured so the GAM-specific bits live in a single Swift package and the app side only knows about thin SwiftUI wrappers.
SampleApp/
├── SampleApp/ ← the iOS app target
│ ├── SampleAppApp.swift ← @main; calls GAMInitializer.start()
│ ├── ContentView.swift ← entry view (just hosts FeedView)
│ ├── Feed/
│ │ ├── Models.swift ← Post, FeedItem (post|ad), AdSlot
│ │ ├── SampleData.swift ← hard-coded posts + ad interleave logic
│ │ ├── PostView.swift ← single post UI
│ │ ├── AdRowView.swift ← single ad UI + per-ad inspector button
│ │ └── FeedView.swift ← scrollable feed
│ └── MyLibrary/ ← Swift package containing all GAM code
│ └── Sources/MyLibrary/
│ ├── GAMConfig.swift ← ad unit ids + app id (one place)
│ ├── GAMInitializer.swift ← MobileAds.shared.start() wrapper
│ ├── GAMBannerAd.swift ← SwiftUI wrapper for AdManagerBannerView
│ ├── GAMAdInspector.swift ← presentAdInspector(...) wrapper
│ └── UIApplication+Root.swift← key window helper
└── SampleApp.xcodeproj
The split is deliberate: everything that imports GoogleMobileAds lives
in MyLibrary. The app target only imports MyLibrary and uses small
SwiftUI types like GAMBannerAd and GAMAdInspector. To swap GAM for a
different ad stack later, you only need to touch MyLibrary.
- Open
SampleApp.xcodeprojin Xcode 26+. - Select an iOS Simulator (or a real device — Ad Inspector works there too).
- Build & run.
On launch you should see Google's "test ad" creatives interleaved with the sample posts. Each ad row carries:
- a Sponsored label and a small format/status badge ("loading", "loaded", "failed");
- an Open Ad Inspector button at the bottom that opens Google's in-app debugging overlay for that specific ad context.
There is also a globe-bug icon in the nav bar that opens Ad Inspector globally (not tied to a single slot).
All ad-related identifiers live in MyLibrary/Sources/MyLibrary/GAMConfig.swift:
| Field | Default | Purpose |
|---|---|---|
applicationIdentifier |
ca-app-pub-3940256099942544~1458002511 |
Test app id, written into Info.plist as GADApplicationIdentifier |
AdUnit.fixedBanner |
/6499/example/banner |
320×50 |
AdUnit.adaptiveBanner |
/21775744923/example/adaptive-banner |
adaptive anchored |
AdUnit.mediumRectangle |
/21775744923/example/medium-rectangle |
300×250 |
feedRotation |
[adaptive, mrec, fixed] |
order in which slots cycle |
To point the app at your own GAM network:
-
Edit
GAMConfig.applicationIdentifierto your AdMob/GAM app id, and updateGADApplicationIdentifierinSampleApp-Info.plistto match.GAMInitializerprints a DEBUG warning if the two drift.⚠️ Don't try to useINFOPLIST_KEY_GADApplicationIdentifierin the pbxproj — Xcode only passes through Apple-recognized keys via that mechanism, andGADApplicationIdentifieris silently dropped. The project uses a realInfo.plist(referenced via theINFOPLIST_FILEbuild setting) to carry that key, while keepingGENERATE_INFOPLIST_FILE = YESso all the standardCFBundle*keys are still auto-generated and merged on top. -
Replace the strings under
GAMConfig.AdUnitwith your own ad unit paths (the/network-code/path/to/unitform). -
Optionally tweak
feedRotationto change which formats appear and in what order.
No other file in the project references ad identifiers, so the GAMConfig
edit is the only thing you need to touch.
SampleData.feed(adEvery:) produces a flat [FeedItem] mixing posts and
ad slots: by default, every 2 posts is followed by an ad. To change cadence,
either pass a different adEvery value or rewrite the function — it's ~20
lines of straight-line Swift.
FeedView is a dumb renderer: it switches on each FeedItem and renders
either a PostView or an AdRowView. There is no network, no view model,
no DI — just data → views.
Ad Inspector is Google's in-app debugger. It shows:
- the most recent ad requests this session,
- the line items returned and the winning creative,
- mediation waterfalls and adapter timings,
- targeting info that was sent on each request.
Two ways to launch it from this app:
- Per-ad button — every
AdRowViewhas its own button. Useful when you want to ask "what exactly got served for that slot?" right after the ad rendered. - Global toolbar button — the ladybug icon in the top-right of the feed opens the inspector regardless of which ad you're looking at.
Both paths funnel through GAMAdInspector.present(...), which calls
MobileAds.shared.presentAdInspector(from:) from the package.
ℹ️ Ad Inspector is gated to test devices. In DEBUG builds the simulator and the running device are auto-registered, so it just works. In Release on a non-registered device, the call returns an error (logged in DEBUG to the Xcode console).
SampleAppApp.init()
└─▶ GAMInitializer.start() ┐
└─▶ MobileAds.shared.start │ runs once per process
└─▶ logs adapter list ┘ (DEBUG only)
GAMInitializer is idempotent: SwiftUI previews and hot-reload won't
re-trigger SDK initialization.
- The project targets iOS 26.1 (Xcode 26). The
MyLibrarypackage is iOS 16+ becauseUIViewRepresentable.sizeThatFitsusesProposedViewSize. GoogleMobileAds13.3.0 is pinned via SPM. Themoloco-sdk-ios-spmdependency is declared but unused — left in place for future experimentation, can be removed fromPackage.swiftif not needed.- App Tracking Transparency (ATT) is not wired up. Test ads don't require it; if you point this at production GAM inventory you'll want to add an ATT prompt and (optionally) UMP for GDPR consent.
- No real images are loaded — posts use SF Symbols on top of gradients to keep the project self-contained.