6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Swift] AdMob を SwiftUI のみでデザインして実装する

Last updated at Posted at 2025-10-04

はじめに

公式では、Swift の AdMob はずっと UIKit しかサポートしていないため、なんとか SwiftUI での実装方法がないか探していました。

結局、結構前に自前で実装したのですが、せっかくなのでメモ程度に残しておきたいと思い書いています。

ちなみにかなり無理やり実現する方法になります。

注意:この方法は正規の方法ではないため、広告がブロックされる可能性もあることに注意してください

実装方法

今回は広告の読み込み部分は特に記載しません。
(どこにでも転がっているので)

SwiftUI のみで広告実装するためにやっている、重要な部分を解説していきます。

0. 既存の実装を見てみる

Google のバナー広告のページに SwiftUI での実装例があります。

UIViewRepresentable で UIKit のラッパーを作る感じですが、結局はその中では Xib or Constraint を使って広告のデザインを実装する必要があり大変面倒です。

1. SwiftUI での実装例

まずは実装例をお見せします。これは私が開発しているアプリでの例です。

(開発画面でのスクリーンショット)

この画面は以下のように SwiftUI で構成されています。

.swift
var adView: some View {
    HStack(alignment: .center, spacing: 16) {
        nativeAdIcon
        VStack(alignment: .leading, spacing: 4) {
            nativeAdTitle
            nativeAdOutline
        }
        Spacer()
        VStack(spacing: 0) {
            nativeAdAdvertiserText
            Spacer()
            nativeAdPRText
        }
    }
}
それぞれの細かいパーツはこちら
.swift
@ViewBuilder
var nativeAdIcon: some View {
    if let icon = adLoader.nativeAdvanceAd?.icon?.image {
        Image(uiImage: icon)
            .resizable()
            .frame(width: 32, height: 32)
            .clipShape(Circle())
    }
}

@ViewBuilder
var nativeAdTitle: some View {
    if let headline = adLoader.nativeAdvanceAd?.headline {
        Text(headline)
            .font(.caption)
            .fontWeight(.bold)
            .foregroundStyle(.appPrimary)
            .lineLimit(1)
    }
}

@ViewBuilder
var nativeAdOutline: some View {
    if let body = adLoader.nativeAdvanceAd?.body {
        Text(body)
            .font(.caption)
            .lineLimit(2)
    }
}

@ViewBuilder
var nativeAdAdvertiserText: some View {
    if let advertiser = adLoader.nativeAdvanceAd?.advertiser {
        Text(advertiser)
            .font(.caption)
            .lineLimit(2)
    }
}

var nativeAdPRText: some View {
    Text("PR")
        .font(.caption2)
        .lineLimit(1)
        .padding(.horizontal, 8)
        .padding(.vertical, 4)
        .background(Color.gray.opacity(0.2))
        .clipShape(RoundedRectangle(cornerRadius: 4))
}

では、これを実現するために、どのようにしているのかを記載していきます。

2. 広告の読み込み部分を作成する

先ほど記載しましたが、こちらは難しくないので、広告の読み込みとそのハンドリング(Delegate)をするリポジトリクラスを作成してください。

以下は、簡易に抜粋したコードです。

.swift
@Observable
final class AdLoader: NSObject {

    private(set) var nativeAdvanceAd: GADNativeAd?
    private var nativeAdvanceAdLoader: GADAdLoader?

    func loadNativeAdvance(unitId: String) {
        guard nativeAdvanceAd == nil else { return }

        nativeAdvanceAdLoader = GADAdLoader(
            adUnitID: unitId,
            rootViewController: application.rootViewController,
            adTypes: [.native],
            options: []
        )
        nativeAdvanceAdLoader?.delegate = self
        nativeAdvanceAdLoader?.load(adRequest) // GADRequest を作る
    }
}

// MARK: - GADAdLoaderDelegate & GADNativeAdLoaderDelegate
extension AdLoader: GADAdLoaderDelegate, GADNativeAdLoaderDelegate {

    // MARK: GADNativeAdLoaderDelegate

    func adLoader(_ adLoader: GADAdLoader,
                  didReceive nativeAd: GADNativeAd) {

        nativeAdvanceAd = nativeAd
    }

    // MARK: GADAdLoaderDelegate

    func adLoader(_ adLoader: GADAdLoader,
                  didFailToReceiveAdWithError error: any Error) {
        // NOP
    }
}

今回は GADNativeAd での実装でやっています。これはあくまでも例なので、自分で使いやすいような形に実装してください。

ポイント:AdLoader@Observable にしており、広告が読み込まれたら SwiftUI 側で画面更新できる作りにしている。

3. 広告のカスタム画面を作成する

今回ここが一番苦労した部分であり、訳ありポイントでもあります。結論を先に述べると、広告の画面を SwiftUI で設計して、その上に透明な広告の UIKit の View を置く形でやっていきます。

以下はわかりやすく広告の部分に色をつけています。赤い部分は UIKit の透明な View です。

Xcode で断面を見るとわかりやすいです。

こういう感じで層として重ねている感じです。では実装を見ていきましょう。まず、広告を受け取ってそれを overlay として表示するラッパーを作ります。

.swift
struct NativeAdView<Content: View>: View {
    let nativeAd: GADNativeAd
    let content: Content

    init(nativeAd: GADNativeAd, @ViewBuilder content: () -> Content) {
        self.nativeAd = nativeAd
        self.content = content()
    }

    var body: some View {
        content
            .overlay {
                NativeAdOverlay(nativeAd: nativeAd)
            }
    }
}

NativeAdOverlay 部分は SwiftUI の上に載せる透明な UIKit の部分です。

.swift
struct NativeAdOverlay: UIViewRepresentable {
    let nativeAd: GADNativeAd

    func makeUIView(context: Context) -> GADNativeAdView {
        let adView = generateNativeAdView()
        // Headline Label
        let headlineLabel = generateHeadlineLabel()
        adView.headlineView = headlineLabel
        adView.addSubview(headlineLabel)
        // AD Label
        let adLabel = generateAdLabel()
        adView.addSubview(adLabel)

        nativeAd.register(
            adView,
            clickableAssetViews: [:],
            nonclickableAssetViews: [:]
        )
        return adView
    }

    func updateUIView(_ uiView: GADNativeAdView, context: Context) {
        // NOP
    }
}

extension NativeAdOverlay {

    func generateNativeAdView() -> GADNativeAdView {
        let adView = GADNativeAdView()
        adView.nativeAd = nativeAd
        return adView
    }

    func generateHeadlineLabel() -> UILabel {
        let label = UILabel()
        label.text = nativeAd.headline
        label.frame = CGRect(x: 0, y: 0, width: 1, height: 1)
        return label
    }

    func generateAdLabel() -> UILabel {
        let label = UILabel()
        label.text = "AD"
        label.frame = CGRect(x: 0, y: 2, width: 1, height: 1)
        return label
    }
}

GADNativeAdView を生成しますが、これは透明な画面としてダミーで作成します。

ポイント:
Google の広告ポリシーで警告が出てしまうため、HeadlineLabel と AdLabel は 最低限ダミーとして生成してセットします。また、画面上で認識させるために 1×1 として見えないように生成し、座標も重ならないように少しだけずらします。

ただし、以下に注意してください。

注意点:
register は広告として認識させるために行なっていますが、特に clickableAssetViews の登録はしていません。つまり、広告のどこがタップされたのか計測できません。

簡単にいうと、1枚の大きい UIKit の View を広告としているためです。

個人アプリ等ではいいですが、実際の企業アプリにおいては、広告のパーツごとの計測をしたい場合に NG かもしれません...

4. 広告の表示

あとは、NativeAdView 内で SwiftUI をつかって好きなように広告のデザインを実装すれば良いです。広告の情報は読み込んだ GADNativeAd から取得できるので、それを元にパーツを作成していきましょう。

実際の呼び出しは以下のような形になると思います。

.swift
struct NativeAdView: View {
    private var adLoader = AdLoader()

    var body: some View {
        Group {
            if let nativeAd = adLoader.nativeAdvanceAd {
                NativeAdView(nativeAd: nativeAd) { 
                    adView // ※ 記事の冒頭にコードあり
                }
            } else {
                ProgressView()
            }
        }
        .onAppear {
            adLoader.loadAd(with: .nativeAdvance)
        }
    }
}

広告の読み込み部分(AdLoader)は、インジェクションせず NativeAdView 内で持っていますが、これは例なので、好きなように実装してください。

余談

記事を書くにあたり後から見つけたのですが、SwiftUI で組んだ全てのパーツの上に、同じ座標を計算して透明な UIKit を載せる実装しているライブラリがありました。

こちらであれば、パーツごとの広告計測もできるのかなと思います。(試していないのでわかりませんが)

終わりに

色々やりましたが、Admob は無理にカスタムせず、無難に UIKit でデザインを組む方がまだ安全かもしれません...

公開していないコードを、公開できる範囲でコピーしてきたので、もし過不足があった場合はコメントもらえたらと思いますmm

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?