```html SwiftUI: How to Build a Banner Ad (iOS 17+, 2026)

How to Build a Banner Ad in SwiftUI

iOS 17+ Xcode 16+ Intermediate APIs: GADBannerView / UIViewRepresentable Updated: May 12, 2026
TL;DR

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

  1. UIViewRepresentable bridge. makeUIView instantiates a GADBannerView with GADAdSizeBanner (320 × 50 pt), sets the ad unit ID, and calls banner.load(GADRequest()) — all the wiring Google's SDK expects.
  2. Coordinator as GADBannerViewDelegate. The Coordinator class adopts GADBannerViewDelegate so that bannerViewDidReceiveAd and didFailToReceiveAdWithError can flip the @Binding<Bool> shared with the SwiftUI layer.
  3. rootViewController lookup. Google's SDK requires a rootViewController to present full-screen ad overlays. The coordinator resolves it lazily from the active UIWindowScene, which is the safe way to do this in a SwiftUI lifecycle app.
  4. Adaptive frame. BannerAdContainerView wraps 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.
  5. 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

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.

```