Skip to content

Latest commit

Β 

History

History
164 lines (125 loc) Β· 6.72 KB

File metadata and controls

164 lines (125 loc) Β· 6.72 KB

SampleApp β€” GAM Experimentation Sandbox

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.


What's in here

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.


Running it

  1. Open SampleApp.xcodeproj in Xcode 26+.
  2. Select an iOS Simulator (or a real device β€” Ad Inspector works there too).
  3. 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).


GAM configuration

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:

  1. Edit GAMConfig.applicationIdentifier to your AdMob/GAM app id, and update GADApplicationIdentifier in SampleApp-Info.plist to match. GAMInitializer prints a DEBUG warning if the two drift.

    ⚠️ Don't try to use INFOPLIST_KEY_GADApplicationIdentifier in the pbxproj β€” Xcode only passes through Apple-recognized keys via that mechanism, and GADApplicationIdentifier is silently dropped. The project uses a real Info.plist (referenced via the INFOPLIST_FILE build setting) to carry that key, while keeping GENERATE_INFOPLIST_FILE = YES so all the standard CFBundle* keys are still auto-generated and merged on top.

  2. Replace the strings under GAMConfig.AdUnit with your own ad unit paths (the /network-code/path/to/unit form).

  3. Optionally tweak feedRotation to 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.


Where ads are inserted

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

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:

  1. Per-ad button β€” every AdRowView has its own button. Useful when you want to ask "what exactly got served for that slot?" right after the ad rendered.
  2. 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).


Initialisation flow

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.


Notes & caveats

  • The project targets iOS 26.1 (Xcode 26). The MyLibrary package is iOS 16+ because UIViewRepresentable.sizeThatFits uses ProposedViewSize.
  • GoogleMobileAds 13.3.0 is pinned via SPM. The moloco-sdk-ios-spm dependency is declared but unused β€” left in place for future experimentation, can be removed from Package.swift if 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.