How to Build a Banner Ad in SwiftUI
SwiftUI has no native banner-ad view, so you bridge Google's GADBannerView into
SwiftUI via UIViewRepresentable. Drop the resulting BannerAdView
anywhere in your layout like any other view.
import GoogleMobileAds
import SwiftUI
struct BannerAdView: UIViewRepresentable {
let adUnitID: String
func makeUIView(context: Context) -> GADBannerView {
let banner = GADBannerView(adSize: GADAdSizeBanner)
banner.adUnitID = adUnitID
banner.rootViewController = context.coordinator.rootVC
banner.load(GADRequest())
return banner
}
func updateUIView(_ uiView: GADBannerView, context: Context) {}
func makeCoordinator() -> Coordinator { Coordinator() }
class Coordinator {
var rootVC: UIViewController? {
UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.first?.windows.first?.rootViewController
}
}
}
Full implementation
The pattern below wires a GADBannerView into SwiftUI using a
UIViewRepresentable wrapper that implements GADBannerViewDelegate
to surface load/fail events into SwiftUI state. A @State boolean controls
whether the banner is visible, preventing layout jank when the ad hasn't loaded yet.
Drop BannerAdContainerView into any screen — it self-sizes to the standard
320 × 50 banner and gracefully collapses if the ad fails to load.
import SwiftUI
import GoogleMobileAds
// MARK: - UIViewRepresentable wrapper
struct BannerAdView: UIViewRepresentable {
let adUnitID: String
@Binding var isLoaded: Bool
func makeCoordinator() -> Coordinator {
Coordinator(isLoaded: $isLoaded)
}
func makeUIView(context: Context) -> GADBannerView {
let banner = GADBannerView(adSize: GADAdSizeBanner) // 320×50
banner.adUnitID = adUnitID
banner.delegate = context.coordinator
banner.rootViewController = context.coordinator.rootViewController
banner.load(GADRequest())
return banner
}
func updateUIView(_ uiView: GADBannerView, context: Context) {}
// MARK: - Coordinator (GADBannerViewDelegate)
final class Coordinator: NSObject, GADBannerViewDelegate {
@Binding var isLoaded: Bool
init(isLoaded: Binding<Bool>) {
_isLoaded = isLoaded
}
var rootViewController: UIViewController? {
UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.first?.keyWindow?.rootViewController
}
// Ad loaded successfully
func bannerViewDidReceiveAd(_ bannerView: GADBannerView) {
isLoaded = true
}
// Ad failed — hide the reserved space
func bannerView(_ bannerView: GADBannerView,
didFailToReceiveAdWithError error: Error) {
isLoaded = false
print("Banner ad failed: \(error.localizedDescription)")
}
}
}
// MARK: - Container view with adaptive height
struct BannerAdContainerView: View {
@State private var isLoaded = false
let adUnitID: String
// Standard banner height is 50pt; collapse to 0 when not loaded
private var bannerHeight: CGFloat { isLoaded ? 50 : 0 }
var body: some View {
BannerAdView(adUnitID: adUnitID, isLoaded: $isLoaded)
.frame(
maxWidth: .infinity,
minHeight: bannerHeight,
maxHeight: bannerHeight
)
.animation(.easeInOut(duration: 0.25), value: isLoaded)
.accessibilityLabel("Advertisement")
.accessibilityHidden(!isLoaded)
}
}
// MARK: - Usage example
struct ContentView: View {
var body: some View {
VStack(spacing: 0) {
ScrollView {
VStack(spacing: 16) {
ForEach(0..<20) { i in
RoundedRectangle(cornerRadius: 12)
.fill(Color.secondary.opacity(0.15))
.frame(height: 80)
.overlay(Text("Card \(i + 1)").font(.headline))
}
}
.padding()
}
Divider()
// Banner anchored at the bottom — Google test unit ID
BannerAdContainerView(
adUnitID: "ca-app-pub-3940256099942544/2934735716"
)
.background(Color(UIColor.systemBackground))
}
}
}
#Preview {
ContentView()
}
How it works
-
UIViewRepresentablebridge.makeUIViewinstantiates aGADBannerViewwithGADAdSizeBanner(320 × 50 pt), sets the ad unit ID, and callsbanner.load(GADRequest())— all the wiring Google's SDK expects. -
Coordinator as
GADBannerViewDelegate. TheCoordinatorclass adoptsGADBannerViewDelegateso thatbannerViewDidReceiveAdanddidFailToReceiveAdWithErrorcan flip the@Binding<Bool>shared with the SwiftUI layer. -
rootViewControllerlookup. Google's SDK requires arootViewControllerto present full-screen ad overlays. The coordinator resolves it lazily from the activeUIWindowScene, which is the safe way to do this in a SwiftUI lifecycle app. -
Adaptive frame.
BannerAdContainerViewwraps the representable in a.frame(maxHeight: bannerHeight)that collapses to 0 while the ad is loading..animation(.easeInOut)makes the reveal smooth once the ad arrives. -
Accessibility.
.accessibilityLabel("Advertisement")gives VoiceOver a meaningful label, and.accessibilityHidden(!isLoaded)keeps the collapsed placeholder from confusing screen-reader users.
Variants
Adaptive / anchored banner (recommended for most apps)
Adaptive banners fill the device width and resize automatically — generally higher eCPM than
fixed-size banners. Replace GADAdSizeBanner in makeUIView:
func makeUIView(context: Context) -> GADBannerView {
// Determine safe width (respects notch / Dynamic Island)
let scene = UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }.first
let safeWidth = scene?.screen.bounds.inset(
by: scene?.keyWindow?.safeAreaInsets ?? .zero
).width ?? 320
let adaptiveSize = GADCurrentOrientationAnchoredAdaptiveBannerAdSizeWithWidth(safeWidth)
let banner = GADBannerView(adSize: adaptiveSize)
banner.adUnitID = adUnitID
banner.delegate = context.coordinator
banner.rootViewController = context.coordinator.rootViewController
banner.load(GADRequest())
return banner
}
// In BannerAdContainerView, drop the fixed height — use intrinsicContentSize instead:
BannerAdView(adUnitID: adUnitID, isLoaded: $isLoaded)
.frame(maxWidth: .infinity)
.frame(height: isLoaded ? nil : 0) // let SDK set intrinsic height
Overlay banner in a ZStack (no layout shift)
If you don't want the banner to push your scroll content up, overlay it instead of stacking
it. Wrap your main content in a ZStack and pin the banner to
.bottom using .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom).
Add matching bottom padding to your ScrollView so content isn't hidden behind
the ad:
.safeAreaPadding(.bottom, isLoaded ? 60 : 0).
Common pitfalls
-
iOS version:
GADMobileAds.sharedInstance().start()must be called once at app launch (in your@main App.init()orAppDelegate.application(_:didFinishLaunchingWithOptions:)) before any ad is requested. Forgetting this produces a silent no-fill. -
Missing
SKAdNetworkentries: Google's SDK requires ~200 SKAdNetwork IDs inInfo.plist. Use the AdMob iOS quick-start guide to get the current list — omitting them won't crash the app but will tank measured conversion rates and eCPM in some regions. -
Performance — never recreate the banner on every SwiftUI re-render.
makeUIViewis called once;updateUIViewis called on every state change. KeepupdateUIViewempty (or minimal) so the banner isn't reloaded unnecessarily, which wastes impressions and ad budget. -
Simulator vs. device: Use the Google test ad unit ID
(
ca-app-pub-3940256099942544/2934735716) during development. Real ad unit IDs return no fill on the simulator and your account can be suspended for generating invalid traffic from production builds during testing.
Prompt this with Claude Code
When using Soarias or Claude Code directly to implement this:
Implement a banner ad in SwiftUI for iOS 17+. Use GADBannerView wrapped in UIViewRepresentable. Make it accessible (VoiceOver labels, accessibilityHidden when not loaded). Collapse the banner frame to 0 height when the ad fails to load. Add a #Preview with realistic sample data showing the banner anchored at the bottom.
In Soarias's Build phase, paste this prompt directly into the Claude Code panel after
scaffolding your monetization screen — the agent will wire the Google Mobile Ads SDK,
generate the UIViewRepresentable bridge, and write the delegate callbacks
without you leaving the editor.
Related
FAQ
Does this work on iOS 16?
The UIViewRepresentable pattern works on iOS 14+, and Google Mobile Ads SDK
9.x supports iOS 16+. However, the code in this guide targets iOS 17+ because it uses
the keyWindow property via UIWindowScene, which behaves
consistently from iOS 17 onward. For iOS 16 support, replace the
keyWindow lookup with .windows.first { $0.isKeyWindow }.
How do I handle GDPR / ATT consent before loading the ad?
Use the Google User Messaging Platform (UMP) SDK. Call
UMPConsentInformation.sharedInstance.requestConsentInfoUpdate on app launch
and only call banner.load(GADRequest()) after the consent form resolves.
Store a @AppStorage("gdprConsent") flag and pass it into
BannerAdView as a parameter — skip the load call if consent
was denied.
What's the UIKit equivalent?
In a UIKit app you'd add a GADBannerView as a subview of your
UIViewController, set rootViewController = self, pin it
to the bottom safe area with Auto Layout, and implement
GADBannerViewDelegate directly on the view controller — no bridge needed.
The UIViewRepresentable wrapper in this guide is specifically the SwiftUI
adaptation of that UIKit pattern.
Last reviewed: 2026-05-12 by the Soarias team.