Add iAB Standard Size Experiences to an iOS app
Show tag-driven IAB banner, medium rectangle, half-page, interstitial, and collapsible ads in an iOS app with the iAB Standard Size Experience SDK 0.0.5.
The iAB Standard Size Experience SDK renders video and audio advertisements from Genuin's ad server inside your own app. You add one Swift package, initialize it with your brand's API key, and place a SwiftUI view that carries a tag ID. The tag identifies an ad placement configured in Brand Control Center, so campaigns, creatives, and targeting change without an app update.
The package https://github.com/genuininc/ios_sdk ships two products as binary xcframeworks:
| Product | Import | Adds | Binary size |
|---|---|---|---|
| GenuinSDK | import GenuinSDK | Genuin.shared, which initializes the platform and holds the API key, consent, and user session | 0.1 MB, plus Core 5.5 MB and _Core 0.1 MB |
| GenuinInStreamAds | import GenuinInStreamAds | The six ad views and the BGInStreamAds.module you register at initialization | 0.2 MB, plus _FeedCore 4.5 MB |
Sizes are the arm64 device binaries in the 0.0.5 package, before App Store compression. Core, _Core, and _FeedCore are shared frameworks that both products list as targets, so adding both products adds them once.
| Ad view | Size (points) | BGAdFormat case | Description |
|---|---|---|---|
| BG320x50AdView | 320 × 50 | .banner320x50 | Compact single-row banner |
| BG320x100AdView | 320 × 100 | .banner320x100 | Large banner |
| BG300x250AdView | 300 × 250 | .mediumRectangle300x250 | Medium rectangle |
| BGCollapsibleAdView | 300 × 250, collapsing to a single row | .collapsibleBanner | Medium rectangle that collapses after playback or when the user taps the chevron |
| BG320x480AdView | 320 × 480 | .portrait320x480 | Mobile interstitial |
| BG300x600AdView | 300 × 600 | .halfPage300x600 | Half page |
Every view fetches the creative for its tag, plays video and audio through Google Interactive Media Ads (IMA), tracks impressions, and handles taps, mute, and full-screen. You pick the view that matches the slot; there's no size argument.
Requirements
| Minimum | |
|---|---|
| iOS | 16.0. Package.swift declares .iOS(.v16) and every framework's MinimumOSVersion is 16.0. |
| Xcode | 26 or later. Package.swift requires Swift tools 6.2; the 0.0.5 frameworks were built with Xcode 26.6 (Swift 6.3.3) with library evolution enabled. |
| Swift | Your target can use Swift 5 or Swift 6 language mode. The frameworks are built in Swift 5 mode with library evolution. |
| Devices | iPhone and iPad (UIDeviceFamily 1 and 2). |
| Architectures | arm64 devices; arm64 and x86_64 simulators. |
| Package manager | Swift Package Manager only. The package contains binary targets, so there's no CocoaPods spec. |
| UI toolkit | SwiftUI. UIKit apps host the views in a UIHostingController. |
| Platforms | iOS only. The package resolves but fails to build for macOS because Google IMA has no macOS build. |
You also need two values from your Genuin account. The API key comes from Brand Control Center under Build > Publish Brand Community > SDK Integration. The tag ID for each ad slot comes from your Genuin team. See Step 5.
Step 1: Add the Genuin package
- In Xcode, go to File > Add Package Dependencies.
- Enter the repository URL
https://github.com/genuininc/ios_sdk. - Set Dependency Rule to Exact Version and enter
0.0.5, the latest release. Up to Next Major Version from0.0.5also accepts0.xpatch releases. - Click Add Package.
- In the product list, select both
GenuinSDKandGenuinInStreamAds, assign them to your app target, and click Add Package.
Xcode downloads the five xcframeworks (GenuinSDK, GenuinInStreamAds, Core, _Core, _FeedCore) and links them as dynamic frameworks.
Step 2: Add the Google IMA package
GenuinInStreamAds links GoogleInteractiveMediaAds.framework, but a binary Swift package can't declare that dependency for you. Add Google's package yourself, or the app crashes at launch:
dyld: Library not loaded: @rpath/GoogleInteractiveMediaAds.framework/GoogleInteractiveMediaAds- Go to File > Add Package Dependencies again.
- Enter
https://github.com/googleads/swift-package-manager-google-interactive-media-ads-ios. - Set Dependency Rule to Branch and enter
main. This is the rule the package'sPackage.swiftdocuments; the latest tagged IMA release is 3.33.0. - Add the
GoogleInteractiveMediaAdsproduct to your app target.
Only IMA is required. The iAB Standard Size Experience SDK doesn't use Google Mobile Ads, so don't add that package.
Step 3: Verify the linked frameworks
Open your app target, go to General > Frameworks, Libraries, and Embedded Content, and confirm that GenuinSDK, GenuinInStreamAds, and GoogleInteractiveMediaAds are listed. Swift Package Manager embeds and signs them during the build, so no other embedding setting is needed.
Step 4: Configure Info.plist and tracking
The SDK reads the App Tracking Transparency (ATT) status and the advertising identifier for ad measurement, but it never shows the ATT prompt. Add the usage description and request authorization yourself:
<key>NSUserTrackingUsageDescription</key>
<string>We use your data to show you more relevant ads.</string>import AppTrackingTransparency
ATTrackingManager.requestTrackingAuthorization { _ in }Request it after launch or at a suitable point in onboarding, before the first ad view appears. Without authorization the identifier is zeroed and ads still serve, but without personalization.
Ad assets load over HTTPS, so App Transport Security needs no changes. Don't enable NSAllowsArbitraryLoads for ad delivery.
Step 5: Get your credentials
| Credential | Purpose | Where to get it |
|---|---|---|
| API key | Authenticates your app. Passed once to initialize. | Brand Control Center > Build > Publish Brand Community > SDK Integration. The screen has a Live Environment and a QA Environment toggle; each environment has its own key. |
| Tag ID | Identifies one ad slot and its campaigns. Passed to each ad view. | The id of an iAB Standard Tag created in Brand Control Center under Grow > Off Site > Extended > iAB Standard Tag and accepted for your brand. Your Genuin team passes it to you. |
The API key is a client-side credential, but keep it out of source control. Put it in an .xcconfig file that your build injects into Info.plist, or in your CI secrets. Read it at runtime with Bundle.main.object(forInfoDictionaryKey:).
Step 6: Initialize the SDK
Call Genuin.shared.initialize once at launch and pass BGInStreamAds.module. Without the module the ad views have no rendering engine and every unit fails with BGTagError.adsManagerUnavailable.
import SwiftUI
import GenuinSDK
import GenuinInStreamAds
@main
struct MyApp: App {
init() {
Genuin.shared.initialize(
modules: [BGInStreamAds.module],
apiKey: "<<YOUR_API_KEY>>",
config: BGSDKConfig(navigationStyle: .system, environment: .prod) // optional; these are the defaults
) { result in
if case .failure(let error) = result { print("Genuin init failed: \(error)") }
}
}
var body: some Scene {
WindowGroup { ContentView() }
}
}In a UIKit app, make the same call in application(_:didFinishLaunchingWithOptions:).
Initialization parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| modules | [Any.Type] | Yes | — | The feature modules to register. Pass [BGInStreamAds.module] for ads. |
| apiKey | String | Yes | — | Your brand's API key for the chosen environment. |
| config | BGSDKConfig | No | .default | environment is .prod or .qa; navigationStyle is .system, .custom, or .hidden and controls the navigation bar of screens the SDK presents. |
| completion | ((Result<Void, Error>) -> Void)? | No | nil | Called once initialization succeeds or fails. |
Note: A QA key only works with environment: .qa. Don't ship a build that points at QA; the two environments have separate brands, tags, and campaigns.
Step 7: Display an ad
Each ad view is a SwiftUI View. Wrap it in BGNavigationRoot, which supplies the navigation container the SDK uses for full-screen playback and landing pages, and pass the tag ID:
import SwiftUI
import GenuinInStreamAds
import Core
struct ContentView: View {
var body: some View {
VStack {
Text("Ad unit").font(.caption).foregroundStyle(.secondary)
BGNavigationRoot {
BG320x50AdView(tagID: "{{TAG_ID}}")
}
}
.padding()
}
}The view fetches the creatives for the tag as soon as it appears. Every view fixes its own height and expands to the width of its container. Place it where the layout provides at least the slot's width: 320 points for the banners and interstitial, 300 points for the medium rectangle and half page. You don't set a frame.
All six views share one initializer:
BG320x50AdView(tagID: String, delegate: (any BGAdDelegate)? = nil)
BG320x100AdView(tagID: "{{TAG_ID}}")
BG300x250AdView(tagID: "{{TAG_ID}}")
BGCollapsibleAdView(tagID: "{{TAG_ID}}")
BG320x480AdView(tagID: "{{TAG_ID}}")
BG300x600AdView(tagID: "{{TAG_ID}}")BGCollapsibleAdView collapses to a single-row banner either on its own after playback (BGCollapseReason.auto) or when the user taps the chevron (.user), and reports both through the delegate.
Host an ad view in UIKit
Wrap the SwiftUI view in a UIHostingController and add it as a child:
import UIKit
import SwiftUI
import GenuinInStreamAds
import Core
let hosting = UIHostingController(rootView: BGNavigationRoot { BG300x250AdView(tagID: "{{TAG_ID}}") })
addChild(hosting)
hosting.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(hosting.view)
NSLayoutConstraint.activate([
hosting.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hosting.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hosting.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
])
hosting.didMove(toParent: self)Initialize the SDK in AppDelegate before the first hosting controller is created.
Step 8: Observe lifecycle events
Pass an object that conforms to BGAdDelegate to react to loading, playback, interaction, and layout events. Every method has an empty default implementation, so implement only the ones you need. BGAdDelegate and its payload types live in _FeedCore, so import it next to GenuinInStreamAds:
import GenuinInStreamAds
import _FeedCore
final class AdTracker: BGAdDelegate {
func iabAdUnitDidLoad(_ unit: BGAdUnitInfo, mediaCount: Int) {
print("Loaded \(mediaCount) item(s) for tag \(unit.tagID ?? "-")")
}
func iabAdUnitDidReceiveNoAds(_ unit: BGAdUnitInfo) { /* hide or resize the slot */ }
func iabAdUnitDidFailToLoad(_ unit: BGAdUnitInfo, error: any Error) {
if let tagError = error as? BGTagError { print("Ad failed: \(tagError)") }
}
func iabAdMediaDidStart(_ mediaItem: BGMediaInfo) { /* impression started */ }
func iabAdAllMediaDidComplete(_ unit: BGAdUnitInfo) { /* the unit played through once */ }
func iabAdCTATapped(_ mediaItem: BGMediaInfo, url: URL) { print("CTA tapped: \(url)") }
}Attach it to a view and keep a strong reference, because the protocol is class-bound and the view doesn't own the delegate:
struct ContentView: View {
@State private var tracker = AdTracker()
var body: some View {
BGNavigationRoot {
BG300x250AdView(tagID: "{{TAG_ID}}", delegate: tracker)
}
}
}Delegate methods
| Method | Payload | Fires when |
|---|---|---|
| iabAdUnitDidStartLoading(_:) | BGAdUnitInfo | The unit started fetching its feed |
| iabAdUnitDidLoad(_:mediaCount:) | BGAdUnitInfo, Int | The feed resolved to one or more renderable items |
| iabAdUnitDidReceiveNoAds(_:) | BGAdUnitInfo | The feed resolved but was empty |
| iabAdUnitDidFailToLoad(_:error:) | BGAdUnitInfo, any Error | The fetch failed; the error is a BGTagError for tag problems |
| iabAdMediaDidAppear(_:) | BGMediaInfo | An item became the active on-screen cell |
| iabAdMediaDidStart(_:) | BGMediaInfo | The creative resolved and playback began |
| iabAdMediaDidFailToFill(_:) | BGMediaInfo | An audio item's ad-slot waterfall produced no fill |
| iabAdMediaDidComplete(_:) | BGMediaInfo | The item's audio or video reached the end |
| iabAdMediaDidChange(from:to:) | BGMediaInfo?, BGMediaInfo | The pager moved between items |
| iabAdAllMediaDidComplete(_:) | BGAdUnitInfo | The last item finished |
| iabAdMuteStateDidChange(_:isMuted:) | BGMediaInfo, Bool | The user muted or unmuted |
| iabAdWatchTapped(_:) | BGMediaInfo | The user tapped Watch |
| iabAdCTATapped(_:url:) | BGMediaInfo, URL | The user tapped the call to action or the advertiser domain |
| iabAdDidEnterFullscreen(_:), iabAdDidExitFullscreen(_:) | BGMediaInfo | Full-screen playback opened or closed |
| iabAdDidCollapse(_:reason:), iabAdDidExpand(_:) | BGMediaInfo, BGCollapseReason | A collapsible unit collapsed (.auto or .user) or expanded |
Payload types
| Type | Fields |
|---|---|
| BGAdUnitInfo | unitID: String, tagID: String?, format: BGAdFormat |
| BGMediaInfo | unit: BGAdUnitInfo, index: Int, mediaCount: Int, mediaID: String, kind: BGAdKind (.sponsored or .audioAd), creative: BGAudioCreative?, mediaItem: BGFeedModel? |
| BGAudioCreative | audioURL, videoURL?, backgroundColorHex, brandLogoURL?, brandName, advertiserDomain, durationSeconds, transcript, vastXML |
| BGTagError | .invalidTagID when the tag ID is empty or malformed; .adsManagerUnavailable when BGInStreamAds.module wasn't passed to initialize |
When a unit fails or has no ads, the view keeps its size and shows a placeholder so your layout doesn't shift. To reclaim the space, hide the slot from iabAdUnitDidReceiveNoAds or iabAdUnitDidFailToLoad.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| The app crashes at launch with dyld: Library not loaded: @rpath/GoogleInteractiveMediaAds.framework | The Google IMA package isn't in the project or isn't linked to the app target | Complete Step 2. |
| Every slot shows the placeholder and iabAdUnitDidFailToLoad reports .adsManagerUnavailable | initialize was called without BGInStreamAds.module, or after the first ad view appeared | Pass the module and initialize in App.init or didFinishLaunchingWithOptions. |
| iabAdUnitDidFailToLoad reports .invalidTagID | The tag ID is empty or malformed | Pass the tag ID exactly as your Genuin team provided it. |
| iabAdUnitDidReceiveNoAds fires for a valid tag | The tag has no active campaign, or the key belongs to the other environment | Check the tag in Brand Control Center and match environment to the key. |
| Build error: no such module GenuinSDK or GenuinInStreamAds | The products are resolved at the project level but not added to the app target, or the deployment target is below iOS 16 | Add both products to the target and set the deployment target to 16.0 or later. |
| Build error: no such module _FeedCore or Core | The file imports a type from a shared framework without importing it | Add import _FeedCore for BGAdDelegate and payload types, import Core for BGNavigationRoot and BGSDKConfig. |
| Resolution or build error mentioning macOS | The target supports macOS or Mac Catalyst | Remove macOS from the target's supported destinations. |
| The ad plays but isn't personalized | ATT authorization wasn't requested or was denied | Request authorization before the first ad view, as in Step 4. |
Limitations
- iOS 16 and later only: The package declares
.iOS(.v16)and the frameworks require it. - SwiftUI first: The ad views are SwiftUI views; UIKit apps need a
UIHostingControllerfor each slot. - IMA is your dependency: Binary targets can't declare it, so every app adds the Google IMA package itself, on branch
main. - No macOS or Mac Catalyst: Google IMA has no macOS build, so the package is iOS only.
- Six fixed sizes: There's no flexible or landscape view in 0.0.5; the Android iAB Standard Size Experience SDK's
BG1280x720AdViewandBGFSLandscapeAdViewhave no iOS counterpart yet. - Tag-only delivery: There's no direct ad-URL parameter; every creative resolves from a tag ID managed in Brand Control Center.
- Binary size: The five frameworks add about 10.4 MB of arm64 code before App Store compression, plus Google IMA.
- Pre-1.0 versioning: Releases are
0.0.xtags; pin an exact version because0.xminor releases can change the API.

