Kivyで作ったiOSアプリにAdMobの広告を表示したときの試行錯誤、の巻

Kivyで作ったiOSアプリにAdMobの広告を表示したときの試行錯誤、の巻

はじめに

PythonのGUIアプリ開発ライブラリであるKivyで作ったアプリをiOSアプリにすることができるんですと。

サンプルコードで試してみるとすんなりiOS化できたのですが、じゃあ今度はそのiOSアプリにAdMobの広告を表示したいぞ、と。でもどうやってやんの???そもそもできるの???となりまして。

で、どうやらできることがわかりました。

これはワタシがKivyで作ったアプリをiOSアプリ化したときにAdMobの広告を表示しようとして、勘違いや回り道をしながら書きなぐった試行錯誤の記録です(独り言を含む)。

いかんせん個人用の記録のつもりのため、とっちらかっていて、正解(と思っている)に辿り着くまでの勘違いや回り道も記述しています(やってみたけど書いてないこともあります)。でももしかしたら誰かの役に立つかもしれないので、うっかりそのままブログ記事にしてしまうことにしました。

なお、検討用として、ボタンを表示するだけのKivyアプリをサンプルとして作って、これをiOSアプリ化して試行錯誤に使っています。んで、本記事(記事と言っていいのかね。。。)の最後にうまくいったときのコードを記載しています。

*ここでは.pyファイル(と.kvファイル)を使ってそのままPythonで走らせるアプリをKivyアプリと呼んで、これをiOSアプリ化したものをiOSアプリと呼ぶことにします。

試行錯誤したときのメモ

実施環境

MacOS Monterey 12.6.3

Xcode Version 14.2 (14C18)

Python 3.9.12(ただし、toolchainが連れてくる?のはPython 3.9.9 )

Kivy 2.1.0(ただし、toolchainが連れてくる?のはKivy 2.2.0 dev0 )

Kivy-iOS 2022.7.19

MacBook Pro(2021), (Apple M1 Pro, メモリ 16GB)

Kivy-iOSを入れてKivyアプリをiOSアプリ化する

まずはKivyアプリをiOSアプリ化しないといけませんので。。。

  • KivyアプリをiOSアプリ化するにはKivy-iOSというライブラリを入れてビルドしないといかん
  • これ https://www.youtube.com/watch?v=ap-pRnBcc5s によると、X86_64環境でやらないといけないっぽい。。。(本当にそうかは未検証です。状況も変わっているかもしれませんし。)
  • と、いうわけで、buildozerを使ってandroidアプリを作った時に使用したX86_64環境下のpyenvコントロール下のpython 3.9.12にて、pipenvによる仮想環境にて実施することにします
    関連記事;macOS Monterey (12.4)のM1 MacBookでBuildozerを使って、Kivyで作ったアプリをapkにしてandroidエミュレータで表示するの巻
  • kivy-ios-buildディレクトリを新たに作ってその中でtoolchainの準備をすることにします
  • 2023.4.27追記ここから
    • arm64環境でもXcodeでKivyアプリのビルドができることを確認しました。
    • ただし、pyenvを使っていると、toolchainでのtoolchain build python3 kivy のステップで、リンクがうまくいかずコケまくります(試せそうなことはあらかた試しましたが何やってもダメでした)。
    • arm64環境でのKivyアプリのビルドは、pyenvをまるまる削除してやって、kivy-iosのgithubのページのインストラクションの通りにvenvを使ってやればうまくいきました。いままでありがとうpyenv。
    • なお、本記事ではX86_64環境(pyenvとpipenvを使用しています)で実施したものを記載しています。
  • 2023.4.27追記ここまで

それでは、

/Users/*****/kivy-ios-build を作っておいて、その中で。。。。(*****はユーザー名です。)

pipenv --python 3.9.12

からの

pipenv shell

からの

pipenv install kivy-ios

からの

xcode-select --install

したら、

xcode-select: error: command line tools are already installed, use "Software Update" to install updates

と言われる。

Command Line Tools for Xcodeのアップデートもあったので、とりあえずアップデートした。

アップデート終わったので続行

brew install autoconf automake libtool pkg-config
brew link libtool

からの

toolchain build python3 kivy

でコンパイル。15:02から開始して15:21に終了。結構かかったな。

では、

あらかじめ作っておいたbuttonを表示するKivyアプリ、

こんな感じの、ただボタンがあるだけのやつ。

kivy_button

の、

main.py

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button

#give class the name of Otameshi 
class Otameshi(BoxLayout):
    pass

#give class the name of ButtontestApp 
class MainApp(App):
    def __init__(self, **kwargs):
        super(MainApp, self).__init__(**kwargs)
    def build(self):
        return Otameshi()
if __name__ == '__main__':
    MainApp().run()

と、main.kv

<Otameshi>
    BoxLayout:
        orientation:'vertical'
        size:root.size
        padding:(100,100)
        Label:
            id: lab1
            text: self.text
            font_size: 80
        Button:
            size: 100, 200
            text: 'zettai osunayo'
            pos: 0,0
            font_size: 80
            on_press: root.ids['lab1'].text='osunatte'
            on_release: root.ids['lab1'].text=''

を作っておいたkivmobtestディレクトリに入れておいて、myfirstappという名前でXcodeのプロジェクトを作ってみます。

toolchain create myfirstapp kivmobtest

と、やってみる。

できたくさいな。

Project directory : myfirstapp-ios
XCode project     : myfirstapp-ios/myfirstapp.xcodeproj

で、、、開くと、

open myfirstapp-ios/myfirstapp.xcodeproj

でけた。初めてやりましたが、ここまでは苦労せずにできました(simulatorで表示しています)。

kivy_ios_app_with_banner

なお、更新したいときはupdateでよさそげ

toolchain update myfirstapp-ios

ここで、Xcodeで

Build Settings > Deployment > iOS Deployment Target
をiOS9.0からiOS11.0に変更しておきました。なんか警告的なメッセージが出るから。

AdMobのBanner広告を入れる試み

ここからが本題です。無事にiOSアプリ化できたので、これにAdMobの広告を表示してみたいぞ、と。

・ストアに登録するアプリを無料アプリ(App内課金なし)にしたいけど少しくらいは収益化もしたいんだもの、ぐへへ => AdMobを入れて広告表示する => AdMobを入れるにはObjective-Cを使う必要がある => Objective-Cを使うにはpyobjusを使う必要がある

・pyobjusは、 https://github.com/kivy/pyobjus/issues/60 にある方法でインストールするのかな?あ、でもビルドするためのレシピがあるのか。で、

pip3 install <https://github.com/kivy/pyobjus/archive/master.z>

=> としてみたけど、インストールする必要なかったようです。Xcodeでのビルド時に勝手に入れてくれるっぽいです

・AdMobのアカウントは既に持っているからAdMobのアカウント作成については本記事ではスルー

・Firebaseのアカウントは既に持っているからFirebaseのアカウント作成については本記事ではスルー

・KivyにAdMobの広告表示をするためにKivMobが必要かと思ったけど(AndroidアプリでのAdMob広告表示にKivMobを使ったから)、 iOSアプリではKivMobは必要なかった

・KivyによるiOSアプリで広告表示する方法についての情報がほとんど見つからなかった

んが、ほとんど情報がなかった中で見つけた、次に示すリンク先の資料を見ながらAdMob(の、banner)を入れてみたいと思います。ああ、作者に感謝。YouTubeでの説明もありますね。

https://docs.google.com/document/d/1NaUxVcO-hGYKiVw1VkaVjqjZShN71zuQZkUZoL4wgJw/edit#

で、どうやら自前でObjective-Cのclassを作ったりしないといけないぽい、と。

おぶじぇくてぃぶしぃ?。。。全くわからん。ああめんどくさい、という状態から始めております。

で、何にせよCocoaPods(iOSアプリ開発言語であるObjective-CとSwift用の、いわゆるライブラリ管理ツールだそうな)というのを入れないといけない。

Homebrewからでよさそげ

brew install cocoapods

うまくいった。ぽい

pod setup

したら

Setup completed

となった。

myfirstapp-ios/

に行きまして

pod init

特になんも言われない。で、Podfileができていた。

Open -a Xcode Podfile

上記の資料の他に、ここも参考にする。

https://firebase.google.com/docs/admob/ios/quick-start

なので、

# Pods for myfirstapp
  pod 'Firebase/Core'
  pod 'Firebase/AdMob'

ではなくて、

# Pods for myfirstapp
  pod 'Google-Mobile-Ads-SDK'

とした。

firebaseの設定ページ(firebaseのアカウントは既に作成しています)に行ってプロジェクトを新規作成した。

iosAdmobtest

というのにしました。

言われるがままにAnalyticsと連携したり。。。で、作成

firebase に入れるAppleバンドルIDは

XcodeのBundle Identifierである、

org.kivy.myfirstapp

を入力することで

「アプリを登録」した。

Firebaseの登録などの作業については、この続きもあるけど後からでいいみたい。

d. Click the Register app button, then return to your Firebase project’s settings screen. You could also follow the subsequent steps in Firebase after pressing Register app, but they are covered in more detail in the following sections in this document.

そしたら

「GoogleService-Info.plist」

をXcodeの

「Resources」

に入れて、「Finish」して、一旦Xcodeプロジェクトを閉じる

で、xprojectプロジェクトのディレクトリ (/Users/*****/kivy-ios-build/myfirstapp-ios)で、

pod install

すると、いろいろ言われるので、対応していく。

(kivy-ios-build) [myfirstapp-ios]$ Pod install
Analyzing dependencies
Adding spec repo `trunk` with CDN `https://cdn.cocoapods.org/`
Downloading dependencies
Installing Google-Mobile-Ads-SDK (9.14.0)
Installing GoogleAppMeasurement (10.4.0)
Installing GoogleUserMessagingPlatform (2.0.1)
Installing GoogleUtilities (7.11.0)
Installing PromisesObjC (2.1.1)
Installing nanopb (2.30909.0)
Generating Pods project
Integrating client project

[!] Please close any current Xcode sessions and use `myfirstapp.xcworkspace` for this project from now on.
Pod installation complete! There is 1 dependency from the Podfile and 6 total pods installed.

[!] Automatically assigning platform `iOS` with version `11.0` on target `myfirstapp` because no platform was specified. Please specify a platform for this target in your Podfile. See `https://guides.cocoapods.org/syntax/podfile.html#platform`.

[!] The `myfirstapp [Debug]` target overrides the `GCC_PREPROCESSOR_DEFINITIONS` build setting defined in `Pods/Target Support Files/Pods-myfirstapp/Pods-myfirstapp.debug.xcconfig'. This can lead to problems with the CocoaPods installation
    - Use the `$(inherited)` flag, or
    - Remove the build settings from the target.

[!] The `myfirstapp [Debug]` target overrides the `HEADER_SEARCH_PATHS` build setting defined in `Pods/Target Support Files/Pods-myfirstapp/Pods-myfirstapp.debug.xcconfig'. This can lead to problems with the CocoaPods installation
    - Use the `$(inherited)` flag, or
    - Remove the build settings from the target.

[!] The `myfirstapp [Debug]` target overrides the `OTHER_LDFLAGS` build setting defined in `Pods/Target Support Files/Pods-myfirstapp/Pods-myfirstapp.debug.xcconfig'. This can lead to problems with the CocoaPods installation
    - Use the `$(inherited)` flag, or
    - Remove the build settings from the target.

[!] The `myfirstapp [Release]` target overrides the `HEADER_SEARCH_PATHS` build setting defined in `Pods/Target Support Files/Pods-myfirstapp/Pods-myfirstapp.release.xcconfig'. This can lead to problems with the CocoaPods installation
    - Use the `$(inherited)` flag, or
    - Remove the build settings from the target.

[!] The `myfirstapp [Release]` target overrides the `OTHER_LDFLAGS` build setting defined in `Pods/Target Support Files/Pods-myfirstapp/Pods-myfirstapp.release.xcconfig'. This can lead to problems with the CocoaPods installation
    - Use the `$(inherited)` flag, or
    - Remove the build settings from the target.

むう、むずかしそう。。。でも1つずつやっていく。

open myfirstapp.xcworkspace

にて、

Enable Modules (C and Objective-C) field is set to Yes.

にするけど、Build Settings > All にありました(Basicでなく)。

ほか、b, c, dは問題なくできた。すでに$(inherited)が入ってたりしたし

Also in Build Settings, add two entries to the Other Linker Flags field: one for -ObjC and one for $(inherited). Use the little + icon to add the new entries.

Similarly, add an entry for $(inherited) in the Framework Search Paths of the Build Settings.

Similarly (again), add an entry for $(inherited) in the Header Search Paths of the Build Settings. I’ve shown a picture below for doing so in the Debug portion, but you’ll probably need it in both Debug and Release.

で、

In the Pods section of your Xcode workspace, go to Build Settings and make sure that the Build Active Architecture Only field is set to No in the Release section (it should be by default). If you don’t see a Pods section, you need to make sure you have opened up your Xcode workspace file, not the project file (refer to step 5c).

これも、もうそのようになってたわ。。。

で、あらためて

pod install

した。すると、出てくるメッセージが変わりました。

(kivy-ios-build) [myfirstapp-ios]$ pod install
Analyzing dependencies
Downloading dependencies
Generating Pods project
Integrating client project
Pod installation complete! There is 1 dependency from the Podfile and 6 total pods installed.

[!] Automatically assigning platform `iOS` with version `11.0` on target `myfirstapp` because no platform was specified. Please specify a platform for this target in your Podfile. See `https://guides.cocoapods.org/syntax/podfile.html#platform`.

[!] The `myfirstapp [Debug]` target overrides the `GCC_PREPROCESSOR_DEFINITIONS` build setting defined in `Pods/Target Support Files/Pods-myfirstapp/Pods-myfirstapp.debug.xcconfig'. This can lead to problems with the CocoaPods installation
    - Use the `$(inherited)` flag, or
    - Remove the build settings from the target.

2個アラートが出てますが、これらは無視してもいいらしい。

気持ち悪いので1個めのやつは11.0にしてみた。Podfile内の指定を9.0から11.0にして

pod install

変わらん。これは意味なかった?。このまま放置する。

続きまして、

7**. Add the required code to your Xcode files**

をやります。

この記述が必要みたい。。。

@property (nonatomic, weak, nullable) UIViewController *rootViewController;

GoogleMobileAds Framework Reference | iOS | Google Developers

んで、該当箇所にこれ入れます。(xxxxxxxxxxxxxxx~yyyyyyyyyyyyyは固有のアプリIDに置き換えてください)

[FIRApp configure];
[GADMobileAds configureWithApplicationID:@"ca-app-pub-xxxxxxxxxxxxxxx~yyyyyyyyyyyyy"];

で、↓これなんだけど、exit(ret);の上に置かないとnever executeでエラーになってしまうので、↓こうしないとだめ。

 Py_Finalize();
    NSLog(@"Leaving");

    [pool release];
    
    [FIRApp configure];
    [[GADMobileAds sharedInstance] startWithCompletionHandler:nil];
    
    // Look like the app still runs even when we left here.
    exit(ret);
    

    return ret;
}

はい、やりました。

あと、deprecatedのエラーが出るので、ここコメントアウトしておきました。

//PyEval_InitThreads();

https://github.com/swig/swig/commit/9c50887daa2b44251e77be8123c63df238034cf5

  1. Create an AdMob account and get your App ID
    をやります。アプリ名「myfirstapp」で追加しました。
  1. Create a banner ad unit
    をほいほいとやりました。この時点でようやくApp IDがわかった。
  2. Insert your App ID and Ad Unit ID into your Xcode project
    をやります。やりました。Test Ad Unit IDのままにしとく。
  3. Add the Code in Python to make the banner display
    をやります。Kivyアプリのコードに追加する形。
  4. Run on an iPhone!
    をやります。(基本シミュレータ上で)

んで、作者のYouTubeの動画でしかやってるのを確認できないやつ。

Signning & Capabilities > Teamを、

***** **** (Personal Team)

にした。(***** ****は自分の名前でした)

つぎに

RunScript > rsyncのところ

rsync -av -O --no-perms ほげほげ 

としてやる(この記事を書いててほげほげってなんだっけってなってます。ごめんなさい。。。)

で、runしました。

ダメ。怒られた。

#import <Firebase/Firebase.h> で、’Firebase/Firebase.h’ file not found

ですって。

#import <GoogleMobileAds/GoogleMobileAds.h>

に変更してみた。

すると、

Account Authentication Failure

There was a problem authenticating your Apple ID.

とかエラーでbuild failする。デベロッパーID持ってないもんな。

Teamのところ、Noneに戻す。

Podfileの

pod GoogleMobleAds

pod ‘Firebase/Core’
pod ‘Firebase/AdMob’

に戻した

で、

pod install
(kivy-ios-build) [myfirstapp-ios]$ Pod install
Analyzing dependencies
Downloading dependencies
Installing Firebase (7.11.0)
Installing FirebaseAnalytics (7.11.0)
Installing FirebaseCore (7.11.0)
Installing FirebaseCoreDiagnostics (7.11.0)
Installing FirebaseInstallations (7.11.0)
Installing Google-Mobile-Ads-SDK 7.69.0 (was 9.14.0)
Installing GoogleAppMeasurement 7.11.0 (was 10.4.0)
Installing GoogleDataTransport (8.4.0)
Installing GoogleUserMessagingPlatform 1.4.0 (was 2.0.1)
Installing GoogleUtilities 7.11.0
Installing PromisesObjC 1.2.12 (was 2.1.1)
Installing nanopb 2.30908.0 (was 2.30909.0)
Generating Pods project
Integrating client project
Pod installation complete! There are 2 dependencies from the Podfile and 12 total pods installed.

[!] Smart quotes were detected and ignored in your Podfile. To avoid issues in the future, you should not use TextEdit for editing it. If you are not using TextEdit, you should turn off smart quotes in your editor of choice.

[!] Automatically assigning platform `iOS` with version `12.0` on target `myfirstapp` because no platform was specified. Please specify a platform for this target in your Podfile. See `https://guides.cocoapods.org/syntax/podfile.html#platform`.

[!] The `myfirstapp [Debug]` target overrides the `GCC_PREPROCESSOR_DEFINITIONS` build setting defined in `Pods/Target Support Files/Pods-myfirstapp/Pods-myfirstapp.debug.xcconfig'. This can lead to problems with the CocoaPods installation
    - Use the `$(inherited)` flag, or
    - Remove the build settings from the target.

で、main.mのやつも

#import <Firebase/Firebase.h>

にもどした。

run

Firebase.h:15:9 ‘FirebaseCore/FirebaseCore.h’ file not found

main.m:11:9 Could not build module ‘Firebase’

ということでござる。。。もう古いのかなあ。2019年の資料だし。。。

#import <FirebaseCore/FirebaseCore.h>

にしてみると?

⇒だめ

#include “FirebaseCore.h”

だと?

⇒だめ

やっぱこれか?

iOSプロジェクトでAdMobを使い始めましょう | Firebase と Google AdMob

と同じ記述にもどして、、、今気づきましたがXcodeから入力できたのね。

pod 'Google-Moble-Ads'

はい。

これをやってみるか。

AppleプロジェクトにFirebaseを追加する | Firebase for Apple platforms

  1. Xcode で、アプリ プロジェクトを開いた状態で、 File > Add Packagesに移動します。

プロンプトが表示されたら、Firebase Apple プラットフォーム SDK リポジトリを追加します。

https://github.com/firebase/firebase-ios-sdk

使用する SDK バージョンを選択します。

**注:**デフォルト (最新) の SDK バージョンを使用することをお勧めしますが、必要に応じて古いバージョンを選択することもできます。

⇒デフォルトで選択されているものを選択しましたよと。

使用する Firebase ライブラリを選択します。

Firebase プロジェクトで Google アナリティクスが有効になっている場合は、必ずFirebaseAnalyticsを追加してください。 IDFA 収集機能のないアナリティクスの場合は、代わりにFirebaseAnalyticsWithoutAdIdを追加してください。

⇒ということで、FirebaseAnalyticsを追加する形で行いました。

なんか進んでる。ログが見れないから心配。。。でもできたぽい。

この時点で

#import <Firebase/Firebase.h>

入れてもやっぱりだめでした。でも、入れたままにしときます。

ま、続けてみる

UIApplicationDelegateFirebaseCore モジュールをインポートし、アプリ デリゲートが使用する他のすべてのFirebase モジュール をインポートします。たとえば、Cloud Firestore と認証を使用するには:

UIApplicationDelegate

などないぞ。

うーん、

一旦もとのドキュメントに立ち戻ってみる

# Pods for myfirstapp
  pod 'Firebase/Core'
  pod 'Firebase/AdMob'
  pod 'FirebaseAnalytics'

にしてみる。

だめ。

じゃあこっちやってみる。

googleads-mobile-ios-examples/Objective-C at main · googleads/googleads-mobile-ios-examples

一旦これまでのPodfileとmain.mに追加していたコードは全部削除

普通にmyfirstapp.xcworkspaceフォルダを削除

あとMacのほうのツールバー(?)の

xcode > preferenceから、

Locations > Derived Data: [Default]のパスの横にある小さい矢印を押して、myfirstappの文字がついている該当ファイル(フォルダ)を探して削除

してから、pod installから再開。またまた仕切り直し。

(kivy-ios-build) [myfirstapp-ios]$ Pod install

普通にbuildしようとしたら、

framework not found FBLPromises エラー

がでる。

これやったら、、、

https://lilea.net/lab/error-framework-not-found-fblpromises/

だめなままでした。

pod入れ直すか。。。

ここ参考に。

https://www.youtube.com/watch?v=zdv9qE4j-VU&list=WL&index=7

なお、上記では

sudo arch -x86_64 gem install ffi
arc -x86_64 pod install

とされているが、わたしはbrewで入れてるから

brew install cocoapods

にてやってみた。

Warning: Treating cocoapods as a formula. For the cask, use homebrew/cask/cocoapods
Warning: cocoapods 1.11.3_1 is already installed, it's just not linked.
To link this version, run:
  brew link cocoapods

と出た。

でも念の為、無視して

brew uninstall cocoapods

先にした。

で、

brew install cocoapods

すると、

Error: The `brew link` step did not complete successfully
The formula built, but is not symlinked into /usr/local
Could not symlink bin/pod
Target /usr/local/bin/pod
already exists. You may want to remove it:
  rm '/usr/local/bin/pod'

To force the link and overwrite all conflicting files:
  brew link --overwrite cocoapods

To list all files that would be deleted:

といわれたので、

brew link --overwrite cocoapods

しました。

Linking /usr/local/Cellar/cocoapods/1.11.3_1... 2 symlinks created.

となって、CocoaPodsの再インストールはOKっぽい。

それでもやっぱビルドエラーが出るんだなぁ。無駄骨だったか。。。

今度は

https://stackoverflow.com/questions/42292090/firebase-undefined-symbols-for-architecture-x86-64

に従って、

/Deleting the ./Pods/  folder and running pod install  works too

をやったけどだめ。

https://firebase.google.com/docs/ios/installation-methods?hl=ja#analytics-enabled

pod 'FirebaseAnalytics'

をやって、

pod install --repo-update

をやって、もだめ。

一旦xcworkspace関連フォルダを削除。preferenceからも。

で再びpod installまで戻って仕切り直し。

Pod install

で作り直した

エラーがこれだけになったな。

Framework not found FBLPromises

で、ここを 「No」にしたらビルドうまく行った。気づかんて。。。

resolve_Framework_not_found_FBLPromises

でも、シミュレータ上でアプリ起動時にエラーでクラッシュ。おぉ。。。

libc++abi: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'GADInvalidInitializationException', reason: 'The Google Mobile Ads SDK was initialized without an application ID. Google AdMob publishers, follow instructions at <https://googlemobileadssdk.page.link/admob-ios-update-plist> to set a valid application ID. Google Ad Manager publishers, follow instructions at <https://googlemobileadssdk.page.link/ad-manager-ios-update-plist.'>
terminating with uncaught exception of type NSException
CoreSimulator 857.14 - Device: iPhone 14 Pro (9751192F-6C19-4826-8E90-7814DB3E1ED7) - Runtime: iOS 16.2 (20C52) - DeviceType: iPhone 14 Pro

メッセージ中に書いてあるここに行ってみよう。

https://googlemobileadssdk.page.link/admob-ios-update-plist

ここかな?

Info.plist を更新する

アプリの Info.plist ファイルを更新して、2 つのキーを追加します。

  1. GADApplicationIdentifier アプリキー。Ad Manager アプリ ID の文字列値を使用します(Ad Manager 管理画面で識別)。
  2. Google の SKAdNetworkIdentifier 値(cstr6suwn9.skadnetwork)を含む SKAdNetworkItems キー。これらの値を Google に提供した追加の購入者を選択します。

2. については、myfirstapp-info.plistに <key>SKAdNetworkItems</key>

以降の部分を手動で追加してみた。あってるのかしら?

次に、

Mobile Ads SDK を初期化する

広告を読み込む前に、[GADMobileAds.sharedInstance](<https://developers.google.com/admob/ios/api/reference/Classes/GADMobileAds#sharedinstance>) の startWithCompletionHandler: メソッドを呼び出します。このメソッドは、SDK が初期化され、初期化の完了後(または 30 秒のタイムアウト後)に完了ハンドラをコールバックします。この処理は 1 回だけ行います(アプリの起動時に行うのが理想的です)。startWithCompletionHandler: はできるだけ早く呼び出す必要があります。

警告: startWithCompletionHandler: の呼び出し時に、Mobile Ads SDK またはメディエーション パートナーの SDK によって広告のプリロードが行われる場合があります。欧州経済領域(EEA)内のユーザーから同意を得る必要がある場合は、リクエスト固有のフラグ(tagForChildDirectedTreatment や tag_for_under_age_of_consent など)を設定するか、広告が読み込まれる前になんらかの対応策を取ったうえで、Mobile Ads SDK を初期化するようにしてください。

AppDelegate で startWithCompletionHandler: メソッドを呼び出す方法の例を次に示します。

だから、ワタシのやつにはAppDelegateがないんですけど。。。

このあたりか?

https://stackoverflow.com/questions/46787801/cant-find-appdelegate-m-in-xcode

ビルドできてもやっぱり起動時に

The Google Mobile Ads SDK was initialized without an application ID.

と怒られてクラッシュ。状況改善せず。

ここの、

https://ios-docs.dev/google-ad-manager/

②Google Ad Managerを追加

Key:GADIsAdManagerApp

Type:Boolean

Value:Yes

で追加

をやってみる。

お、エラーが変わったぞ。当たりか?

"/Users/*****/Library/Developer/CoreSimulator/Devices/9751192F-6C19-4826-8E90-7814DB3E1ED7/data/Containers/Bundle/Application/52749A78-C90B-4CA6-A03B-7C1867299C4D/myfirstapp.app/lib/python3.9/site-packages/kivy/app.py", line 949, in _run_prepare
     self.dispatch('on_start')
   File "kivy/_event.pyx", line 731, in kivy._event.EventDispatcher.dispatch
   File "/Users/*****/kivy-ios-build/myfirstapp-ios/YourApp/main.py", line 21, in on_start
   File "pyobjus/pyobjus.pyx", line 751, in pyobjus.pyobjus.autoclass
   File "pyobjus/pyobjus.pyx", line 98, in pyobjus.pyobjus.MetaObjcClass.__new__
   File "pyobjus/pyobjus.pyx", line 129, in pyobjus.pyobjus.MetaObjcClass.resolve_class
 pyobjus.pyobjus.ObjcException: Unable to find class b'adSwitch'
2023-01-24 14:08:54.319322+0900 myfirstapp[72034:1037745] Application quit abnormally!
2023-01-24 14:08:54.341184+0900 myfirstapp[72034:1037745] Leaving

ああそうか。

main.pyのon_startのここだけ潰してみた。

#self.banner_ad = autoclass('adSwitch').alloc().init()

起動した!!!!

deprecatedになっている「GADRequestError」の部分、ええと、

// An error occurred
- (void)adView:(GADBannerView *)view didFailToReceiveAdWithError:(GADRequestError *)error


- (void)bannerView:(nonnull GADBannerView *)bannerView didFailToReceiveAdWithError:(nonnull NSError *)error;

に変更。

あと、ここ

バナー広告 | iOS | Google Developers

を参考にごちゃごちゃやりました。すると。。。

ぐおー、とにかくバナー広告が出たぞ!寝る。

kivy_ios_app_original

起きた。実機検証してみる。iPhone 7です。参考ここ

[iPhone] 実機でiOSアプリを確認する | iOS アプリ開発

で、実機検証しようとしたらエラーが出た。ふお!?

対処法これでうまくいった。回答が2つあるが、両方やること。

Getting error line 132: ARCHS[@]: unbound variable

デベロッパーを信頼するっていうのをやらんといかん。

できた。

これ実機(iPhone 7)のスクリーンショットです。ちゃんとバナー出ました。やったね。

kivy_ios_app_with_banner_on_real_iphone7

結局。。。

AdMobのbannerの表示させる方法で、今のところ唯一見つかった方法は、本記事で既出のこれ。

https://docs.google.com/document/d/1NaUxVcO-hGYKiVw1VkaVjqjZShN71zuQZkUZoL4wgJw/edit#

ただし、ちょっと古いせいか(2019年に作成されている)、少し記述を変えないといけなかった。

上記とセットで使うものとして、main.mに入れるためのスニペット(Objective Cによる、AdMobのbanner広告用のclass)がここにある。

https://gist.github.com/Dirk-Sandberg/e20854cc833b8a13708892f4ef0909b3

またはこのfork。ちょっとだけ記述が違うところがある。どっちがいいかわからん。更新時期は同じくらい。2022.12ぽい。 => こっちのforkが今は正解のようでしたのでこっちを参考にします。https://gist.github.com/shirubei/65cb741eadd64a71d5e7cc3eaaf5567e

https://github.com/shirubei/simcalWithAdmobs

リンク先にも書いてあるけど <Firebase/Firebase.h>ではなくて、

#import <GoogleMobileAds/GoogleMobileAds.h>

にしておく必要がある。

んで、これはinterstitial広告/banner用のPython側のコード。

https://gist.github.com/dbrova15/fc221679f210d047e52d69149ec1558b

main.mへ挿入するsnipetについてもあるので、詳しくは直上のリンクページのリンクを辿ってください。

最終的に、上記を参考にして

interstitialとreward interstitialも表示することができるようになりました。

ATT(App Tracking Transparency)対応

・AdMobで広告表示するならATT(App Tracking Transparency)対応しないといかん 。

えーてーてー??。。。と、いう状態から始めております。

⇒ GoogleのUser Messaging Platform(UMP)で対応できることがわかりました。

が、ここから先しばらくは、UMPを使えばいいということに気づく前の話です。。。役に立たないけどなんか忍びないので残してます。

参考にしたページ群。

Objective-C: How to display permission to track in iOS 14.5 and above in Objective-C?

requestTrackingAuthorizationWithCompletionHandlerでATTダイアログが表示されない

個人アプリ開発日誌:AppTrackingTransparency対応した – koogawa blog

オプション機能 | Analytics for iOS | Google Developers

やってみよう

pod 'GoogleIDFASupport'

を記載しておいてからの

Pod update

で、

Xcodeのmyfirstapp-info.plistで、

Privacy - Tracking Usage Description

を追加し、Valueに文言を追加。

AppDelegate.m(ここらへん https://github.com/firebase/quickstart-ios/blob/master/admob/AdMobExample/AppDelegate.m を参考に無理くりファイルを作りました。でも結局は要らなかったことに気づく。)にて

#import <AppTrackingTransparency/AppTrackingTransparency.h> 

を追加しておいて、

- (void)applicationDidBecomeActive:(UIApplication *)application {
    
    if (@available(iOS 14, *)) {
        // Display permission to track
        [ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {
            switch(status) {
                case ATTrackingManagerAuthorizationStatusNotDetermined :
                    NSLog(@"Unknown consent");
                case ATTrackingManagerAuthorizationStatusRestricted :
                    NSLog(@"Device has an MDM solution applied");
                case ATTrackingManagerAuthorizationStatusDenied :
                    NSLog(@"Denied consent");
                case ATTrackingManagerAuthorizationStatusAuthorized :
                    NSLog(@"Granted consent");
                default :
                    NSLog(@"Unknown");
            }
        }];
    }
}

とした。

うまくいかん。ええと、

‘app_didenterforeground’に対応するSDLのイベントを調べてみると、

SDL eventWhatiOSAndroidWinRT
SDL_APP_TERMINATINGThe application is being terminated by the OS.applicationWillTerminate()onDestroy()Exiting()
SDL_APP_LOWMEMORYThe application is low on memory, free memory if possible.applicationDidReceiveMemoryWarning()onLowMemory()
SDL_APP_WILLENTERBACKGROUNDThe application is about to enter the background.applicationWillResignActive()onPause()Suspending()
SDL_APP_DIDENTERBACKGROUNDThe application did enter the background and may not get CPU for some time.applicationDidEnterBackground()onPause()Suspending()
SDL_APP_WILLENTERFOREGROUNDThe application is about to enter the foreground.applicationWillEnterForeground()onResume()Resuming()
SDL_APP_DIDENTERFOREGROUNDThe application is now interactive.applicationDidBecomeActive()onResume()Resuming()
https://wiki.libsdl.org/SDL2/SDL_EventType

on_resumeに相当するイベント? ⇒ はいそのようです。

でもアプリ起動時には発動しない。。。なぜだ。

アプリがbackgroundからforegroundに戻ってきたときのon_resumeでは発動するけども。。。これじゃだめやん。

タイミングとして、起動時に(広告表示前に)確認メッセージを出さないといけない(はず)ので。

Kivyの、

kivy/window_sdl2.py at master · kivy/kivy

を見てみると、、、

def _event_filter(self, action):
        from kivy.app import App
        if action == 'app_terminating':
            EventLoop.quit = True
            self.close()

        elif action == 'app_lowmemory':
            self.dispatch('on_memorywarning')

        elif action == 'app_willenterbackground':
            from kivy.base import stopTouchApp
            app = App.get_running_app()
            if not app:
                Logger.info('WindowSDL: No running App found, exit.')
                stopTouchApp()
                return 0

            if not app.dispatch('on_pause'):
                Logger.info('WindowSDL: App doesn\\'t support pause mode, stop.')
                stopTouchApp()
                return 0

            self._pause_loop = True

        elif action == 'app_didenterforeground':
            # on iOS, the did enter foreground is launched at the start
            # of the application. in our case, we want it only when the app
            # is resumed
            if self._pause_loop:
                self._pause_loop = False
                app = App.get_running_app()
                app.dispatch('on_resume')

        return 0

うーんどうしよう。これじゃ起動時に発動しないわな。。。

。。。調べていくと、、、UMPが使えることに気づきます。。。ああ回り道。回り道したやつは全部削除してから(せっかく作ってみたAppDelegate.mもいらないので削除)、

ここから先はUMPを使うことに気づいた後の話です。

おお、既視感。。。Androidアプリ作ったときに似たようなのやったわ。

iOS 14 以降に備える | Google Developers

キーポイント:

App Tracking Transparency(ATT)フレームワークをアプリに含める場合は、User Messaging Platform(UMP)SDK を使って

IDFA 説明メッセージをトリガー

し、この許可を求める理由をユーザーに示すことができます。なお、UMP SDK の使用は、アプリの全ユーザーに影響するためご注意ください。

UMP SDK を使用していない場合は、このページの残りの部分で、OS レベルの ATT 許可リクエストを手動で実装する方法について説明します。

からの、これ。iOSでもできるんやん。

ユーザーメッセージングプラットフォームとの同意の取得 | UMP SDK for iOS | Google Developers

はじめに

UMP SDK には、パブリッシャーがパーソナライズド広告の同意をリクエストし、Apple の App Tracking Transparency(ATT)要件を処理するためのツールが用意されています。すべての設定はの AdMob の [プライバシーとメッセージ] で行われるため、パブリッシャーは UMP SDK を使用して、これらのフォームのいずれかまたは両方を処理できます。

Google の EU ユーザーの同意ポリシーでは、英国および欧州経済領域(EEA)のユーザーに特定の情報を開示するとともに、Cookie またはその他のローカル ストレージを(法律上必要な場合)使用すること、および個人データ(AdID など)を使用して広告を配信することについて、ユーザーの同意を得る必要があります。このポリシーには、EU の e プライバシー指令と一般データ保護規則(GDPR)の要件が反映されています。

パブリッシャー様がこのポリシーで定められた義務を遂行できるよう、Google は以前のオープンソースの Consent SDK に代わる、User Messaging Platform(UMP)SDK を提供しています。UMP SDK が更新され、最新の IAB 標準をサポートするようになりました。また、同意フォームの設定と広告パートナーの一覧表示のプロセスも簡素化しました。上記の設定はすべて、AdMob の [プライバシーとメッセージ] で簡単に処理できます。

このガイドでは、SDK のインストール、IAB ソリューションの実装、テスト機能の有効化の方法について説明します。

ここにUMP SDKによるIDFAとGDPRとATTの表示についての説明の表が。

About IDFA messages

NSUserTrackingUsageDescriptionは重複してると言われる。。。

どうやら、

Privacy – Tracking Usage Description

になっているみたいだ。あら?すでに入力済みでした。

いろいろこねくり回したがうまくいかん。メッセージが出てこない。

ここに解決策が。

UMPConsentInformation.sharedInstance.formStatus always UMPFormStatusUnAvailable

AdMobでのメッセージの設定が必要だったみたい。ああ完全に忘れていた。

AdMobの設定ページの

「プライバシーとメッセージ」から

設定した。で、24時間ほど待つらしい。

いま2023.1.26.1:07

まつまつ。

2023.1.27. 7:12で公開済みになっていた。

ので、ちょっとやってみる。

まだエラーでてメッセージ表示されない。

待つか。。。

2023.1.27. 12:01にちょっとやってみる。

まだエラーでてメッセージ表示されない。

あと12時間くらい待つか。。。

2023.1.27. 22:30にちょっとやってみる。コンソールに出力されてるエラーメッセージが変わった気がする。でも気力がないので今日はこれ以上の確認しない。ああ何か間違っているのだろうか。

これって、pyファイルのmainのところで実行をやらないといかんて事か? ⇒そういうわけではなさそう。Xcodeで見れるiOSアプリ用のファイルのmain.mでのことを言っているようだ。

このメソッドは、メインスレッドからのみ呼び出す必要があります。

https://www.notion.so/iOS-button-banner-04fb2165313f4beb945d8aa294ad792e#2ed59e3bb34246aa89d2715c21463ade

ここを参考にしたけどだめ。

Google unified messaging platform SDK implementation

んで、、、、

on_resumeで広告表示の処理を入れたら、UMPのconsent formがでた!

consent formで一旦pause状態(ちなみにiOSではon_pause関数と紐づけて実行すると落ちる。)になって、formの確認が終わるとon_resumeが発動するからか。。。

で、熊の動画(熊の動画が出てきてたのよなぜか)の代わりにちゃんとした動画広告が出た。なぜかわからんけど。consent formがちゃんと出た後は熊の動画が出なくなるようだ。。。

次に、on_startのところで実行するようにしてもうまくいった。おお、なんでや。これで起動した時にも出るようになった。なぜかはよくわからん。。。

それから、アプリを上にスワイプして終了した後に、再度アプリを起動した時にはon_resumeではなくon_startが発動するので、on_startで広告が表示されるようにしておかないといけない。

でも、on_startに広告表示処理を入れたのはいいけど、これでは初回起動時においてもConsentを得る前に広告を表示/リクエストしてしまうことになるから、なんか分岐を入れて処理を切り分けないといけない。。。

Consentの状態で切り分けるか。。。

ちなみに、これでConsentStatusを確認できる。(main.pyに入れ込む)

Consentstatus = autoclass('UMPConsentInformation')
print(Consentstatus.sharedInstance().consentStatus)

で、

https://developers.google.com/admob/ios/privacy/api/reference/Enums/UMPConsentStatus.html

によると、

Consentstatusが 0 = unknown(初回起動時はこれになる)

Consentstatusが 1 = consent required but not yet obtained : (地域を EEAに設定しておくと起動時には1になる)

Consentstatusが 2 = consent not required

Consentstatusが 3 = consent obtained

んで、ATTのメッセージフォームをこなした後の2回目以降の起動では、

Consentstatusは2か3になるみたい。

じゃあConsentstatusが2以上かどうかで切り分ければいいか。

ちなみに、Consent informationのリセットの処理を入れてると毎回Consent formが出て来ますので、毎回のリセットが必要なければコメントアウトしておく。

//Reset consent information
    [UMPConsentInformation.sharedInstance reset];

結果

はい、うまくいった模様です。アプリはボタンを押して広告を出したり消したりロードしたりできるように改変してます。

うまくいった時のコード

コードはこちら。

main.py

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.utils import platform
from kivy.logger import Logger

#give class the name of Otameshi 
class Otameshi(BoxLayout):
    pass

#give class the name of ButtontestApp 
class MainApp(App):
    def __init__(self, **kwargs):
        super(MainApp, self).__init__(**kwargs)
        if platform =='ios':
            Logger.info("myfirstapp: __init__ done")

    def build(self):
        if platform =='ios':
            self.otameshi = Otameshi()
            return (self.otameshi)

    def on_resume(self):
        if platform =='ios':
            from pyobjus import autoclass
            self.banner_ad = autoclass('adSwitch').alloc().init()
            self.interstitial_ad = autoclass('myInterstitial').alloc().init()
            self.rewarded_video = autoclass('myRewardedvideo').alloc().init()
            self.rewarded_video.loadRewardedAd()

            self.consentstatus = autoclass('UMPConsentInformation').sharedInstance().consentStatus

    def on_start(self):
        if platform =='ios':
            from pyobjus import autoclass
            Logger.info("myfirstapp:check ConsentStatus")
            self.consentstatus = autoclass('UMPConsentInformation').sharedInstance().consentStatus
            #Consentstatus = 0 ; unknown
            #Consentstatus = 1 ; consent required but not yet obtained
            #Consentstatus = 2 ; consent not required 
            #Consentstatus = 3 ; consent obtained
            #show ads when Consentstatus = 2 or 3
            if self.consentstatus >= 2:
                Logger.info("myfirstapp:check ConsentStatus; >=2 ")
                self.banner_ad = autoclass('adSwitch').alloc().init()
                self.interstitial_ad = autoclass('myInterstitial').alloc().init()
                self.rewarded_video = autoclass('myRewardedvideo').alloc().init()
                self.rewarded_video.loadRewardedAd()
            else:
                Logger.info("myfirstapp:check ConsentStatus; < 2")

            #show consent form
            AttConsent = autoclass('AttConsent')
            self.attConsent = AttConsent.alloc().init()
            self.attConsent.consentcheck()
            Logger.info("myfirstapp: end of loadForm()")
        else:
            pass

    def show_banner(self):
        if platform =='ios':
            Logger.info("myfirstapp: execute show_banner()")
            from pyobjus import autoclass
            #self.banner_ad = autoclass('adSwitch').alloc().init()
            self.banner_ad.show_ads()

    def show_interstitial(self):
        if platform =='ios':
            Logger.info("myfirstapp: execute show_interstitial()")
            self.interstitial_ad.createAndLoadInterstitial()
            self.interstitial_ad.InterstitialView()

    def hide_banner(self):
        if platform =='ios':
            self.banner_ad.hide_ads()

    def load_rewarded_video(self):
        if platform =='ios':
            Logger.info("myfirstapp: execute load_rewarded_video()")
            self.rewarded_video.loadRewardedAd()

    def show_rewarded_video(self):
        if platform =='ios':
            Logger.info("myfirstapp: execute show_rewarded_video()")
            self.rewarded_video.show_video()

if __name__ == '__main__':
    MainApp().run()

main.kv

<Otameshi>
    BoxLayout:
        orientation:'vertical'
        size:root.size
        padding:(100,100)
        Label:
            id: lab1
            text: self.text
            font_size: 80
        Button:
            size: 100, 200
            text: 'hide banner'
            pos: 0,0
            font_size: 80
            on_release:
                app.hide_banner()
        Button:
            size: 100, 200
            text: 'show banner'
            pos: 0,0
            font_size: 80
            on_release:
                app.show_banner()
        Button:
            size: 100, 200
            text: 'show interstitial'
            pos: 0,0
            font_size: 80
            on_release:
                app.show_interstitial()
        Button:
            size: 100, 200
            text: 'show rewarded video'
            pos: 0,0
            font_size: 80
            on_release:
                app.show_rewarded_video()
        Button:
            size: 100, 200
            text: 'prepare rewarded video'
            pos: 0,0
            font_size: 80
            on_press:
                root.ids['lab1'].text='osunatte'
            on_release: 
                root.ids['lab1'].text='';\
                app.load_rewarded_video()

main.m (*****のところは内緒)

//
//  main.m
//  myfirstapp
//

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#include "Python.h"
#include "/Users/*****/kivy-ios-build/dist/include/common/sdl2/SDL_main.h"
#include <dlfcn.h>
#import <GoogleMobileAds/GoogleMobileAds.h>
#import <FirebaseCore/FirebaseCore.h> // needed for FIRApp
#include <UserMessagingPlatform/UserMessagingPlatform.h> //needed for ATT, GDPR consent form



UIView *gView;
UIViewController *gViewController;
@interface myBanner : NSObject <GADBannerViewDelegate>

@property (nonatomic) BOOL show_ads;
@property (strong, nonatomic) GADBannerView *bannerView;
@property (nonatomic, weak, nullable) UIViewController *rootViewController;
@property (strong, nonatomic) GADRequest *request;


@end

static myBanner *vbanner = nil;

@implementation myBanner


-(id)init {

    // admob allocation
    NSLog(@"Creating google banner object");
    self.request = [GADRequest request];

    // I'm not sure this is even necessary
    
    GADMobileAds.sharedInstance.requestConfiguration.testDeviceIdentifiers = @[ GADSimulatorID, @"2077ef9a63d2b398840261c8221a0c9b" ];// Sample device ID

    UIWindow *window = [UIApplication sharedApplication].keyWindow;
    UIViewController *rootViewController = window.rootViewController;

    gViewController = rootViewController;//[[SDLLaunchScreenController alloc] init];
    gView = rootViewController.view; ///gViewController.view;

    //create and show banner from here
    // Create a view of the standard size at the top or the bottom of the screen.
    // Available AdSize constants are explained in GADAdSize.h.
    self.bannerView = [[GADBannerView alloc] initWithAdSize:GADAdSizeBanner];

    CGSize AdSize = GADAdSizeBanner.size;

    CGRect frame = self.bannerView.frame;
    frame.origin.x = (gViewController.view.bounds.size.width - AdSize.width) / 2 ;

    //frame.origin.y = 0.0f; //on Top
    frame.origin.y = gViewController.view.bounds.size.height - AdSize.height; //on Bottom

    self.bannerView.frame = frame;

    [self.bannerView setDelegate:self];

    // Specify the ad's "unit identifier." The default ID is for Google’s test banner ad. If everything works and you see a google test ad, you have implemented everything correctly.
    self.bannerView.adUnitID = @"ca-app-pub-3940256099942544/2934735716"; // google's test id for banner ads

    //  ------------- Height and position of the banner ad
    //CGRect screenBounds = [[UIScreen mainScreen] bounds];
    //[self.bannerView setFrame:CGRectMake(0, 0, screenBounds.size.width, 1.5*self.bannerView.bounds.size.height)];
    //self.bannerView.center = CGPointMake(screenBounds.size.width / 2, screenBounds.size.height - (self.bannerView.bounds.size.height / 2));

    //self.bannerView.hidden = TRUE;
    // Let the runtime know which UIViewController to restore after taking
    // the user wherever the ad goes and add it to the view hierarchy.
    self.bannerView.rootViewController = gViewController;

    [gView addSubview:self.bannerView];

    [self.bannerView loadRequest:self.request];

    self.show_ads = TRUE;


    return self;
}


// Called before ad is shown, good time to show the add
- (void)bannerViewDidReceiveAd:(GADBannerView *)bannerView
{
    NSLog(@"Admob load");
    self.bannerView.hidden = !self.show_ads;
}

// An error occurred
- (void)bannerView:(nonnull GADBannerView *)bannerView didFailToReceiveAdWithError:(nonnull NSError *)error
{
    NSLog(@"Admob error: %@", error);
    self.bannerView.hidden = TRUE;
}


-(void)dealloc {
    NSLog(@"Freeing ads");
    if (self.bannerView) {
        [self.bannerView removeFromSuperview];
        [self.bannerView release];
        self.bannerView.delegate = nil;
        self.bannerView = nil;
    }
    [super dealloc];
}

- (void)showAds:(int)ontop {
    self.show_ads = TRUE;

    NSLog(@"Displaying banner object ontop:%d.", ontop);

    CGSize AdSize = GADAdSizeBanner.size;

    CGRect frame = self.bannerView.frame;
    frame.origin.x = (gViewController.view.bounds.size.width - AdSize.width) / 2 ;

    if (ontop)
        frame.origin.y = 0.0f;
    else
        frame.origin.y = gViewController.view.bounds.size.height - AdSize.height;

    self.bannerView.frame = frame;

}

@end

@interface adSwitch : NSObject
@end
@implementation adSwitch

-(id)init {
    if (!vbanner)
    {
        vbanner = [[myBanner alloc] init];

        [vbanner showAds:0]; //0: ontop

    }
    return self;
}

-(void) show_ads {
    if (!vbanner)
        vbanner = [[myBanner alloc] init];

    [vbanner showAds:0];//0: ontop

}

-(void) hide_ads {
    if (vbanner)
    {

        [vbanner release];
        vbanner = nil;
    }
}
@end

//myBanner end

//myInterstitial from here
@interface myInterstitial : NSObject <GADFullScreenPresentingAd>
@property (nonatomic, weak, nullable) UIViewController *rootViewController;
@property(nonatomic, strong) GADInterstitialAd *interstitial;
@property (strong, nonatomic) GADRequest *request;

@end

@implementation myInterstitial

-(id)init {

    // admob allocation
    NSLog(@"Creating google Interstitial object");

    //interstitial from here
    [self createAndLoadInterstitial];
    NSLog(@"createAndInterstitial");
    //interstitial end
    // I'm not sure this is even necessary
 
    GADMobileAds.sharedInstance.requestConfiguration.testDeviceIdentifiers = @[ GADSimulatorID, @"2077ef9a63d2b398840261c8221a0c9b" ];// Sample device ID

    UIWindow *window = [UIApplication sharedApplication].keyWindow;
    UIViewController *rootViewController = window.rootViewController;

    gViewController = rootViewController;//[[SDLLaunchScreenController alloc] init];
    gView = rootViewController.view; ///gViewController.view;
    
    return self;
}
@synthesize fullScreenContentDelegate;


//interstitial here
- (void)createAndLoadInterstitial {
    //GADRequest *request = [GADRequest request];

    [GADInterstitialAd loadWithAdUnitID:@"ca-app-pub-3940256099942544/4411468910" request:_request completionHandler:^(GADInterstitialAd *ad, NSError *error) {
        if (error) {
            NSLog(@"Failed to load interstitial ad with error: %@", [error localizedDescription]);
            return;
        }
        self.interstitial = ad;
        [self.interstitial.fullScreenContentDelegate self];
        self.request = [GADRequest request];

        //self.interstitial.fullScreenContentDelegate = self;
    }]; // test id


    NSLog(@"createAndLoadInterstitial done");
}


/// Tells the delegate that the ad failed to present full screen content.
- (void)ad:(nonnull id<GADFullScreenPresentingAd>)ad
didFailToPresentFullScreenContentWithError:(nonnull NSError *)error {
    NSLog(@"Ad did fail to present full screen content.");
}

/// Tells the delegate that the ad will present full screen content.
- (void)adWillPresentFullScreenContent:(nonnull id<GADFullScreenPresentingAd>)ad {
    NSLog(@"Ad will present full screen content.");
}

/// Tells the delegate that the ad dismissed full screen content.
- (void)adDidDismissFullScreenContent:(nonnull id<GADFullScreenPresentingAd>)ad {
   NSLog(@"Ad did dismiss full screen content.");
}

- (void)interstitialWillDismissScreen:(GADInterstitialAd *)ad {
    //  Method for reloading the object so that you can show ads again
    NSLog(@"interstitialWillDismissScreen");
    [self createAndLoadInterstitial];
}

- (void)InterstitialView {  // show interstitial ADS
    if (self.interstitial) {
        NSLog(@"Show interstitial ADS!");
        UIWindow *window = [UIApplication sharedApplication].keyWindow;
        UIViewController *rootViewController = window.rootViewController;
        [self.interstitial presentFromRootViewController:rootViewController];
    } else {
        NSLog(@"interstitial Ad wasn't ready");
    }
}

@end

//myInterstitial end

//myRewardedvideo from here

@interface myRewardedvideo : NSObject <GADFullScreenContentDelegate>
/// The text indicating current coin count.
@property(weak, nonatomic) IBOutlet UILabel *coinCountLabel;
@property(nonatomic, strong) GADRewardedAd *rewardedAd;
@property (strong, nonatomic) GADRequest *request;
@property (nonatomic, weak, nullable) UIViewController *rootViewController;

@end


@implementation myRewardedvideo

-(id)init {

    // admob allocation
    NSLog(@"Creating google rewarded video object");
    //self.request = [GADRequest request];

    //NSLog(@"loadRewardedAd done");

    // I'm not sure this is even necessary
    //GADMobileAds.sharedInstance.requestConfiguration.testDeviceIdentifiers = @[@"Simulator"];
    GADMobileAds.sharedInstance.requestConfiguration.testDeviceIdentifiers = @[ GADSimulatorID, @"2077ef9a63d2b398840261c8221a0c9b" ];// Sample device ID

    UIWindow *window = [UIApplication sharedApplication].keyWindow;
    UIViewController *rootViewController = window.rootViewController;

    gViewController = rootViewController;//[[SDLLaunchScreenController alloc] init];
    gView = rootViewController.view; ///gViewController.view;

    return self;
}

- (void)loadRewardedAd {
  //GADRequest *request = [GADRequest request];
    [GADRewardedAd loadWithAdUnitID:@"ca-app-pub-3940256099942544/1712485313" request:_request completionHandler:^(GADRewardedAd *ad, NSError *error) {

        if (error) {
          NSLog(@"Rewarded ad failed to load with error: %@", [error localizedDescription]);
          return;
        }
        self.rewardedAd = ad;
        NSLog(@"Rewarded ad loaded.");
        //self.rewardedAd.fullScreenContentDelegate = self;
        [self.rewardedAd.fullScreenContentDelegate self];
        self.request = [GADRequest request];

      }];
}


/// Tells the delegate that the ad failed to present full screen content.
- (void)ad:(nonnull id<GADFullScreenPresentingAd>)ad
didFailToPresentFullScreenContentWithError:(nonnull NSError *)error {
    NSLog(@"Ad did fail to present full screen content.");
}

/// Tells the delegate that the ad will present full screen content.
- (void)adWillPresentFullScreenContent:(nonnull id<GADFullScreenPresentingAd>)ad {
    NSLog(@"Ad will present full screen content.");
}

/// Tells the delegate that the ad dismissed full screen content.
- (void)adDidDismissFullScreenContent:(nonnull id<GADFullScreenPresentingAd>)ad {
   NSLog(@"Ad did dismiss full screen content.");
    [self loadRewardedAd]; //load ad when dismissed
}


- (void)show_video { // show video ad
  NSLog(@"execute show_video");

  if (self.rewardedAd) {
     UIWindow *window = [UIApplication sharedApplication].keyWindow;
     UIViewController *rootViewController = window.rootViewController;
    [self.rewardedAd presentFromRootViewController:rootViewController userDidEarnRewardHandler:^{GADAdReward *reward = self.rewardedAd.adReward;
    NSLog(@"ToDo Reward the user. reward is %@", reward);
        // TODO: Reward the user!
                                }];
  } else {
    NSLog(@"Ad wasn't ready");
  }
}

@end
//myRewardedvideo end



@interface AttConsent : NSObject 
//@interface AttConsent : UIViewController 
@property (nonatomic, weak, nullable) UIViewController *rootViewController;
//@property (strong, nonatomic) GADRequest *request;
@end

@implementation AttConsent

-(id)init {    
    UIWindow *window = [UIApplication sharedApplication].keyWindow;
    UIViewController *rootViewController = window.rootViewController;

    gViewController = rootViewController;//[[SDLLaunchScreenController alloc] init];
    
    return self;

}

- (void)consentcheck {
    NSLog(@"start consentcheck");

    // Create a UMPRequestParameters object.
    UMPRequestParameters *parameters = [[UMPRequestParameters alloc] init];
    UMPDebugSettings *debugSettings = [[UMPDebugSettings alloc] init];

    //debugSettings.testDeviceIdentifiers = @[ @"TEST-DEVICE-HASHED-ID" ];

    debugSettings.geography = UMPDebugGeographyEEA; //set region EAA
    parameters.debugSettings = debugSettings;
    //NSLog(@"parameters.debugSettings; %@", parameters.debugSettings);
    //NSLog(@"debugSettings.geography; %ld", debugSettings.geography);

    // Set tag for under age of consent. Here NO means users are not under age.
    parameters.tagForUnderAgeOfConsent = NO;
    NSLog(@"parameters.tagForUnderAgeOfConsent; %d", parameters.tagForUnderAgeOfConsent);

    //Reset consent information
    //[UMPConsentInformation.sharedInstance reset];

    // Request an update to the consent information.
    [UMPConsentInformation.sharedInstance requestConsentInfoUpdateWithParameters:parameters completionHandler:^(NSError *_Nullable error) {
        if (error) {
            NSLog(@"error in consentcheck");
            // Handle the error.
        } else {
            NSLog(@"else in consentcheck, Got UMPConsentInformation then proceed");

            // The consent information state was updated.
            // You are now ready to check if a form is
            // available.
            UMPFormStatus formStatus = UMPConsentInformation.sharedInstance.formStatus;
            if (formStatus == UMPFormStatusAvailable) {
                NSLog(@"else then if in consentcheck, execute loadForm");
                [self loadForm];
            }
            else{
                NSLog(@"UMPFormStatusUnknown or UMPFormStatusUnavailable, then don't proceed loadForm method");
            }
        }
    }];
}


- (void)loadForm {
  [UMPConsentForm loadWithCompletionHandler:^(UMPConsentForm *form, NSError *loadError) {
    if (loadError) {
        NSLog(@"loaderror in loadForm ");

      // Handle the error.
    } else {
        NSLog(@"else in loadForm ");

      // Present the form. You can also hold on to the reference to present
      // later.
      if (UMPConsentInformation.sharedInstance.consentStatus ==
          UMPConsentStatusRequired) {
          NSLog(@"UMPConsentInformation.sharedInstance.consentStatus%ld", UMPConsentInformation.sharedInstance.consentStatus);

          [form presentFromViewController:gViewController
                    completionHandler:^(NSError *_Nullable dismissError) {
                      if (UMPConsentInformation.sharedInstance.consentStatus ==
                          UMPConsentStatusObtained) {        
                          // App can start requesting ads.
                          [[myRewardedvideo alloc] init];
                          //[[myRewardedvideo alloc] loadRewardedAd];
                          [[adSwitch alloc] init];
                          [[adSwitch alloc] show_ads];
                          [[myInterstitial alloc] init];
                          NSLog(@"admob prepared!");

                      } else {NSLog(@"not obtain consent yet");}

                    }];
      } else {
          NSLog(@"nothing done in loadForm function");
        // Keep the form available for changes to user consent.
      }
    }
  }];
}


@end


void export_orientation(void);
void load_custom_builtin_importer(void);

int main(int argc, char *argv[]) {

    int ret = 0;

    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    // Change the executing path to YourApp
    chdir("YourApp");

    // Special environment to prefer .pyo, and don't write bytecode if .py are found
    // because the process will not have a write attribute on the device.
    putenv("PYTHONOPTIMIZE=2");
    putenv("PYTHONDONTWRITEBYTECODE=1");
    putenv("PYTHONNOUSERSITE=1");
    putenv("PYTHONPATH=.");
    putenv("PYTHONUNBUFFERED=1");
    putenv("LC_CTYPE=UTF-8");
    // putenv("PYTHONVERBOSE=1");
    // putenv("PYOBJUS_DEBUG=1");

    // Kivy environment to prefer some implementation on iOS platform
    putenv("KIVY_BUILD=ios");
    putenv("KIVY_WINDOW=sdl2");
    putenv("KIVY_IMAGE=imageio,tex,gif,sdl2");
    putenv("KIVY_AUDIO=sdl2");
    putenv("KIVY_GL_BACKEND=sdl2");

    // IOS_IS_WINDOWED=True disables fullscreen and then statusbar is shown
    putenv("IOS_IS_WINDOWED=False");

    #ifndef DEBUG
    putenv("KIVY_NO_CONSOLELOG=1");
    #endif

    // Export orientation preferences for Kivy
    export_orientation();

    NSString * resourcePath = [[NSBundle mainBundle] resourcePath];
    NSString *python_home = [NSString stringWithFormat:@"PYTHONHOME=%@", resourcePath, nil];
    putenv((char *)[python_home UTF8String]);

    NSString *python_path = [NSString stringWithFormat:@"PYTHONPATH=%@:%@/lib/python3.9/:%@/lib/python3.9/site-packages:.", resourcePath, resourcePath, resourcePath, nil];
    putenv((char *)[python_path UTF8String]);

    NSString *tmp_path = [NSString stringWithFormat:@"TMP=%@/tmp", resourcePath, nil];
    putenv((char *)[tmp_path UTF8String]);

    NSLog(@"Initializing python");
    Py_Initialize();

    wchar_t** python_argv = PyMem_RawMalloc(sizeof(wchar_t *) *argc);
    for (int i = 0; i < argc; i++)
        python_argv[i] = Py_DecodeLocale(argv[i], NULL);
    PySys_SetArgv(argc, python_argv);

    // If other modules are using the thread, we need to initialize them before.
    //PyEval_InitThreads();

    // Add an importer for builtin modules
    load_custom_builtin_importer();

    // Search and start main.py
#define MAIN_EXT @"pyc"

    const char * prog = [
        [[NSBundle mainBundle] pathForResource:@"YourApp/main" ofType:MAIN_EXT] cStringUsingEncoding:
        NSUTF8StringEncoding];
    NSLog(@"Running main.py: %s", prog);
    FILE* fd = fopen(prog, "r");
    if ( fd == NULL ) {
        ret = 1;
        NSLog(@"Unable to open main.py, abort.");
    } else {
        ret = PyRun_SimpleFileEx(fd, prog, 1);
        if (ret != 0)
            NSLog(@"Application quit abnormally!");
    }

    Py_Finalize();
    NSLog(@"Leaving");

    [pool release];

    [FIRApp configure];
    [[GADMobileAds sharedInstance] startWithCompletionHandler:nil];


    // Look like the app still runs even when we left here.
    exit(ret);

    return ret;
}




// This method reads the available orientations from the Info.plist file and
// shares them via an environment variable. Kivy will automatically set the
// orientation according to this environment value, if it exists. To restrict
// the allowed orientation, please see the comments inside.
void export_orientation() {
    NSDictionary *info = [[NSBundle mainBundle] infoDictionary];
    NSArray *orientations = [info objectForKey:@"UISupportedInterfaceOrientations"];

    // Orientation restrictions
    // ========================
    // Comment or uncomment blocks 1-3 in order the limit orientation support

    // 1. Landscape only
    // NSString *result = [[NSString alloc] initWithString:@"KIVY_ORIENTATION=LandscapeLeft LandscapeRight"];

    // 2. Portrait only
    // NSString *result = [[NSString alloc] initWithString:@"KIVY_ORIENTATION=Portrait PortraitUpsideDown"];

    // 3. All orientations
    NSString *result = [[NSString alloc] initWithString:@"KIVY_ORIENTATION="];
    for (int i = 0; i < [orientations count]; i++) {
        NSString *item = [orientations objectAtIndex:i];
        item = [item substringFromIndex:22];
        if (i > 0)
            result = [result stringByAppendingString:@" "];
        result = [result stringByAppendingString:item];
    }
    // ========================

    putenv((char *)[result UTF8String]);
    NSLog(@"Available orientation: %@", result);
}

void load_custom_builtin_importer() {
    static const char *custom_builtin_importer = \
        "import sys, imp, types\n" \
        "from os import environ\n" \
        "from os.path import exists, join\n" \
        "try:\n" \
        "    # python 3\n"
        "    import _imp\n" \
        "    EXTS = _imp.extension_suffixes()\n" \
        "    sys.modules['subprocess'] = types.ModuleType(name='subprocess')\n" \
        "    sys.modules['subprocess'].PIPE = None\n" \
        "    sys.modules['subprocess'].STDOUT = None\n" \
        "    sys.modules['subprocess'].DEVNULL = None\n" \
        "    sys.modules['subprocess'].CalledProcessError = Exception\n" \
        "    sys.modules['subprocess'].check_output = None\n" \
        "except ImportError:\n" \
        "    EXTS = ['.so']\n"
        "# Fake redirection to supress console output\n" \
        "if environ.get('KIVY_NO_CONSOLE', '0') == '1':\n" \
        "    class fakestd(object):\n" \
        "        def write(self, *args, **kw): pass\n" \
        "        def flush(self, *args, **kw): pass\n" \
        "    sys.stdout = fakestd()\n" \
        "    sys.stderr = fakestd()\n" \
        "# Custom builtin importer for precompiled modules\n" \
        "class CustomBuiltinImporter(object):\n" \
        "    def find_module(self, fullname, mpath=None):\n" \
        "        # print(f'find_module() fullname={fullname} mpath={mpath}')\n" \
        "        if '.' not in fullname:\n" \
        "            return\n" \
        "        if not mpath:\n" \
        "            return\n" \
        "        part = fullname.rsplit('.')[-1]\n" \
        "        for ext in EXTS:\n" \
        "           fn = join(list(mpath)[0], '{}{}'.format(part, ext))\n" \
        "           # print('find_module() {}'.format(fn))\n" \
        "           if exists(fn):\n" \
        "               return self\n" \
        "        return\n" \
        "    def load_module(self, fullname):\n" \
        "        f = fullname.replace('.', '_')\n" \
        "        mod = sys.modules.get(f)\n" \
        "        if mod is None:\n" \
        "            # print('LOAD DYNAMIC', f, sys.modules.keys())\n" \
        "            try:\n" \
        "                mod = imp.load_dynamic(f, f)\n" \
        "            except ImportError:\n" \
        "                # import traceback; traceback.print_exc();\n" \
        "                # print('LOAD DYNAMIC FALLBACK', fullname)\n" \
        "                mod = imp.load_dynamic(fullname, fullname)\n" \
        "            sys.modules[fullname] = mod\n" \
        "            return mod\n" \
        "        return mod\n" \
        "sys.meta_path.insert(0, CustomBuiltinImporter())";
    PyRun_SimpleString(custom_builtin_importer);
}

main.mについては、

#import <GoogleMobileAds/GoogleMobileAds.h>

から

(わあごめんなさい間違ってました。。。2023.4.20修正ここから。)

// This method reads the available orientations from the Info.plist file and

の直上までのコードを、Xcodeのプロジェクトを作った時に生成されたmain.mの相当する部分に挿入すればOKです。

void export_orientation(void);
void load_custom_builtin_importer(void);

の直上までのコードを、Xcodeのプロジェクトを作った時に生成されたmain.mの相当する部分に挿入し、

    [FIRApp configure];
    [[GADMobileAds sharedInstance] startWithCompletionHandler:nil];

を該当部分に挿入すればOKです。

(2023.4.20修正ここまで。)

なお、Xcodeの各種設定は本記事のどこか、または参照リンク先のどこかをご参照ください。

おわりに

はい、以上、Kivyで作ったiOSアプリにAdMobの広告を表示したときの試行錯誤の記録でした。

何でもかんでも書きなぐっているので分かりにくいところも多々あると思いますが、そこはすみません。

コードについては、ご興味のある方はライセンス遵守下でご自由にお使いください。

それで、色々試行錯誤した結果、Kivyで作ったiOSアプリにAdMobの広告を表示できて、ATTとGDPRの同意フォームも表示できた、というところまではいいのですが、ただこのままだとGDPRのconsentを拒否された場合には広告が全く出なくなってしまうご様子。

全く広告が出せないのはあれなので、せめてパーソナライズされていない広告を出せる最低限の同意が得られたときに、パーソナライズされていない広告を出せるようにはしたい。方法はあるはずなのですが、なんかムズそう。。。

んで、ワタシが初めて作ったKivyを使ったAndroidアプリ(myRoupeiro – サッカーの試合記録と分析 – for amazon)をiOSアプリ化してApp Storeにも並べてみたいなと考えているのですが、そのために解決しないといけないことが今回の広告表示以外にもまだまだあるので道のりは長そうです。。。その道のりで何かブログに残しておいてみようかなというものが出て来たらまた書こうと思います。

最後までお読みいただきありがとうございました。

おしまい。

本記事に記載したコードのライセンス

本記事に記載したコードのライセンスは次のとおりとさせてください。

main.pyおよびmain.kvのコード

MIT License

Copyright (c) 2023 bu

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

main.mのコード

ApacheLicense2.0

Copyright 2023 bu

Licensed under the Apache License, Version 2.0 (the “License”);

you may not use this file except in compliance with the License.

You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

当記事のコードの元になっているコード

main.pyのベースになっているコード

Pythonのコードの広告表示に関連している部分は次に示す資料を参考に、

https://docs.google.com/document/d/1NaUxVcO-hGYKiVw1VkaVjqjZShN71zuQZkUZoL4wgJw/edit#

次のリンク先に関連しているコード群をベースにして作製しました。

https://github.com/shirubei/simcalWithAdmobs

MIT License

Copyright (c) 2022 shirubei

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

main.mのベースになっているもの

main.mは

https://docs.google.com/document/d/1NaUxVcO-hGYKiVw1VkaVjqjZShN71zuQZkUZoL4wgJw/edit#

https://gist.github.com/shirubei/65cb741eadd64a71d5e7cc3eaaf5567e

の記載を参考にしながら、

AdMob | Google Developers

のコード群をベースにして作製しました。