はじめに
公式では、Swift の AdMob はずっと UIKit しかサポートしていないため、なんとか SwiftUI での実装方法がないか探していました。
結局、結構前に自前で実装したのですが、せっかくなのでメモ程度に残しておきたいと思い書いています。
ちなみにかなり無理やり実現する方法になります。
注意:この方法は正規の方法ではないため、広告がブロックされる可能性もあることに注意してください
実装方法
今回は広告の読み込み部分は特に記載しません。
(どこにでも転がっているので)
SwiftUI のみで広告実装するためにやっている、重要な部分を解説していきます。
0. 既存の実装を見てみる
Google のバナー広告のページに SwiftUI での実装例があります。
UIViewRepresentable
で UIKit のラッパーを作る感じですが、結局はその中では Xib or Constraint を使って広告のデザインを実装する必要があり大変面倒です。
1. SwiftUI での実装例
まずは実装例をお見せします。これは私が開発しているアプリでの例です。
(開発画面でのスクリーンショット)
この画面は以下のように SwiftUI で構成されています。
var adView: some View {
HStack(alignment: .center, spacing: 16) {
nativeAdIcon
VStack(alignment: .leading, spacing: 4) {
nativeAdTitle
nativeAdOutline
}
Spacer()
VStack(spacing: 0) {
nativeAdAdvertiserText
Spacer()
nativeAdPRText
}
}
}
それぞれの細かいパーツはこちら
@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)をするリポジトリクラスを作成してください。
以下は、簡易に抜粋したコードです。
@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
として表示するラッパーを作ります。
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 の部分です。
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
から取得できるので、それを元にパーツを作成していきましょう。
実際の呼び出しは以下のような形になると思います。
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