この記事は Applibot Advent Calendar 2025 24日目の記事です。
はじめに
Unity で iOS 向けのネイティブプラグインを開発する中で「ネイティブのビュー関連のイベントを受け取る方法」について調査していると、iOS ビルドで生成される Xcode プロジェクト内の Classes/PluginBase に含まれるリスナー機能を利用すれば、簡単に実現できることが分かりました。
具体的には以下 3 つの機能が提供されており、本記事ではこれらの概要とプラグインの実装例について解説します。
| 機能 | 概要 |
|---|---|
UnityViewControllerListener |
UIViewController に関連するライフサイクルイベント |
LifeCycleListener |
アプリのライフサイクルイベント |
AppDelegateListener |
UIApplicationDelegate に関連するシステムイベント |
検証環境
- Unity
2022.3.62f2,6000.0.64f1,6000.3.1f1 - Xcode 26.2
各種リスナーについて
UnityViewControllerListener
UnityViewControllerListener は、UIViewController に関連するライフサイクルイベントを受け取るためのリスナーです。3
こちらを用いれば「ネイティブ側のレイアウトが変更された際のイベント」や、「他の UIViewController に切り替えた際のイベント」などを検知できるようになります。
プロトコルの定義と受け取れるイベント
こちらは Classes/PluginBase/UnityViewControllerListener.h に定義されており、一部引用すると以下のようなプロトコル4が定義されてます。
#pragma once
#import <Foundation/NSNotification.h>
// view changes on the main view controller
@protocol UnityViewControllerListener<NSObject>
@optional
- (void)viewWillLayoutSubviews:(NSNotification*)notification;
- (void)viewDidLayoutSubviews:(NSNotification*)notification;
- (void)viewWillDisappear:(NSNotification*)notification;
- (void)viewDidDisappear:(NSNotification*)notification;
- (void)viewWillAppear:(NSNotification*)notification;
- (void)viewDidAppear:(NSNotification*)notification;
- (void)interfaceWillChangeOrientation:(NSNotification*)notification;
- (void)interfaceDidChangeOrientation:(NSNotification*)notification;
@end
// 中略
void UnityRegisterViewControllerListener(id<UnityViewControllerListener> obj);
void UnityUnregisterViewControllerListener(id<UnityViewControllerListener> obj);
詳細は後述しますが、基本的にはこちらのプロトコルを実装したクラスをプラグイン側で用意し、そのクラスのインスタンスを UnityRegisterViewControllerListener に渡して登録する流れとなります。
また、イベント自体もネイティブ側の UIViewController が持つものとほぼ同一であり、具体的には以下のようなイベントを受け取ることができます。5
| メソッド | 対応するイベント |
|---|---|
viewWillLayoutSubviews |
UIViewController.viewWillLayoutSubviews() |
viewDidLayoutSubviews |
UIViewController.viewDidLayoutSubviews() |
viewWillAppear |
UIViewController.viewWillAppear(_:) |
viewDidAppear |
UIViewController.viewDidAppear(_:) |
viewWillDisappear |
UIViewController.viewWillDisappear(_:) |
viewDidDisappear |
UIViewController.viewDidDisappear(_:) |
LifeCycleListener
LifeCycleListener は、アプリのライフサイクルイベントを受け取るためのリスナーです。
具体的に言うと「バックグラウンド <-> フォアグラウンド移行時のイベント」や「アプリ終了時に発火されるイベント」などの検知が可能です。
一応 Unity 標準でもバックグラウンド <-> フォアグラウンド移行の大まかなタイミングであれば OnApplicationPause などを用いることで検知することが可能ですが、さらに細かいタイミングで制御が必要な場合には覚えておくと活用できるかもしれません。
プロトコルの定義と受け取れるイベント
こちらは Classes/PluginBase/LifeCycleListener.h に定義されており、一部引用すると以下のようなプロトコルが定義されてます。
#pragma once
// important app life-cycle events
@protocol LifeCycleListener<NSObject>
@optional
- (void)didFinishLaunching:(NSNotification*)notification;
- (void)didBecomeActive:(NSNotification*)notification;
- (void)willResignActive:(NSNotification*)notification;
- (void)didEnterBackground:(NSNotification*)notification;
- (void)willEnterForeground:(NSNotification*)notification;
- (void)willTerminate:(NSNotification*)notification;
- (void)unityDidUnload:(NSNotification*)notification;
- (void)unityDidQuit:(NSNotification*)notification;
@end
void UnityRegisterLifeCycleListener(id<LifeCycleListener> obj);
void UnityUnregisterLifeCycleListener(id<LifeCycleListener> obj);
イベントの方は LifeCycleListener.mm のコードを読むと、OS 標準で定義されている以下の通知を登録していることが確認できました。
| メソッド | 対応するイベント・通知 |
|---|---|
didFinishLaunching |
UIApplicationDidFinishLaunchingNotification |
didBecomeActive |
UIApplicationDidBecomeActiveNotification |
willResignActive |
UIApplicationWillResignActiveNotification |
didEnterBackground |
UIApplicationDidEnterBackgroundNotification |
willEnterForeground |
UIApplicationWillEnterForegroundNotification |
willTerminate |
UIApplicationWillTerminateNotification |
また、これ以外にも Unity 独自で定義したイベントも持っており、内部的にアンロードが走るタイミングでイベントが発火されているのが確認できました。
| メソッド | 説明 |
|---|---|
unityDidUnload |
Unity がアンロードされた直後に呼ばれる |
unityDidQuit |
Unity が終了した直後に呼ばれる |
AppDelegateListener
AppDelegateListener は、UIApplicationDelegate に関連するシステムイベントを受け取るためのリスナーです。
こちらのプロトコルは前章で解説した LifeCycleListener を継承しており、LifeCycleListenerのイベントに加えて、以下の UIApplicationDelegate のイベントも受け取ることができます。
プロトコルの定義と受け取れるイベント
こちらは Classes/PluginBase/AppDelegateListener.h に定義されており、一部引用すると以下のようなプロトコルが定義されてます。
#pragma once
#include "LifeCycleListener.h"
@protocol AppDelegateListener<LifeCycleListener>
@optional
// these do not have apple defined notifications, so we use our own notifications
// notification will be posted from
// - (BOOL)application:(UIApplication*)application openURL:(NSURL*)url sourceApplication:(NSString*)sourceApplication annotation:(id)annotation
// notification user data is the NSDictionary containing all the params
- (void)onOpenURL:(NSNotification*)notification;
// notification will be posted from
// - (BOOL)application:(UIApplication*)application willFinishLaunchingWithOptions:(NSDictionary*)launchOptions
// notification user data is the NSDictionary containing launchOptions
- (void)applicationWillFinishLaunchingWithOptions:(NSNotification*)notification;
// notification will be posted from
// - (void)application:(UIApplication*)application handleEventsForBackgroundURLSession:(nonnull NSString *)identifier completionHandler:(nonnull void (^)())completionHandler
// notification user data is NSDictionary with one item where key is session identifier and value is completion handler
- (void)onHandleEventsForBackgroundURLSession:(NSNotification*)notification;
// these are just hooks to existing notifications
- (void)applicationDidReceiveMemoryWarning:(NSNotification*)notification;
- (void)applicationSignificantTimeChange:(NSNotification*)notification;
- (void)applicationWillChangeStatusBarFrame:(NSNotification*)notification;
- (void)applicationWillChangeStatusBarOrientation:(NSNotification*)notification;
@end
void UnityRegisterAppDelegateListener(id<AppDelegateListener> obj);
void UnityUnregisterAppDelegateListener(id<AppDelegateListener> obj);
受け取ることができるイベントは OS 標準で定義されている通知に加え、幾つかは Unity が内部的に実装している UIApplicationDelegate の実装クラス (UnityAppController.mm 辺り) より呼び出されます。
| メソッド | 対応するイベント・通知 |
|---|---|
onOpenURL |
UIApplicationDelegate.application:openURL:options: |
applicationWillFinishLaunchingWithOptions |
UIApplicationDelegate.application:willFinishLaunchingWithOptions: |
onHandleEventsForBackgroundURLSession |
UIApplicationDelegate.application:handleEventsForBackgroundURLSession:completionHandler: |
applicationDidReceiveMemoryWarning |
UIApplicationDidReceiveMemoryWarningNotification |
applicationSignificantTimeChange |
UIApplicationSignificantTimeChangeNotification |
applicationWillChangeStatusBarFrame |
UIApplicationWillChangeStatusBarFrameNotification |
applicationWillChangeStatusBarOrientation |
UIApplicationWillChangeStatusBarOrientationNotification |
Unity で活用する際の実装例
それでは、実際に Unity でこれらのリスナーを活用する方法について簡単に解説していきます。
本記事では UnityViewControllerListener を中心に解説しますが、他のリスナーも同様のパターンで実装されています。
また、今回解説したリスナーの登録周りを汎用的にしたパッケージの方も公開しており、こちらの方を実装サンプルとして参考にしていただけると幸いです。
(執筆時点では v1.0.3 をベースに解説)
ネイティブプラグインの実装
まずはイベントを受け取るためのクラスとして、UnityViewControllerListener.h に定義されている UnityViewControllerListener プロトコルを実装したクラスを用意します。6
#include "PluginBase/UnityViewControllerListener.h"
#include <stdint.h>
// view changes on the main view controller
typedef void (*ViewWillLayoutSubviewsCallback)(void* context);
typedef void (*ViewDidLayoutSubviewsCallback)(void* context);
// (中略)
@interface UnityViewControllerListenerBridge : NSObject<UnityViewControllerListener>
@property (nonatomic, assign) ViewWillLayoutSubviewsCallback viewWillLayoutSubviewsCallback;
@property (nonatomic, assign) ViewDidLayoutSubviewsCallback viewDidLayoutSubviewsCallback;
// (中略)
@end
@implementation UnityViewControllerListenerBridge
- (void)viewWillLayoutSubviews:(NSNotification*)notification
{
if (self.viewWillLayoutSubviewsCallback) {
self.viewWillLayoutSubviewsCallback((__bridge void*)self);
}
}
- (void)viewDidLayoutSubviews:(NSNotification*)notification
{
if (self.viewDidLayoutSubviewsCallback) {
self.viewDidLayoutSubviewsCallback((__bridge void*)self);
}
}
// (中略)
あとはこちらを P/Invoke から呼び出すために、上述のクラスを生成・破棄するメソッドと、 UnityViewControllerListener.h にある UnityRegisterViewControllerListener, UnityUnregisterViewControllerListener へ登録するためのメソッドも用意します。
#ifdef __cplusplus
extern "C" {
#endif
// イベントを受け取るためのクラスの生成
void* iOSUtility_NativeEventListener_CreateUnityViewControllerListenerBridge(
ViewWillLayoutSubviewsCallback viewWillLayoutSubviewsCallback,
ViewDidLayoutSubviewsCallback viewDidLayoutSubviewsCallback,
// (中略)
{
// 生成したクラスに対し、引数から渡されたコールバックを登録していく
UnityViewControllerListenerBridge* bridge = [[UnityViewControllerListenerBridge alloc] init];
bridge.viewWillLayoutSubviewsCallback = viewWillLayoutSubviewsCallback;
bridge.viewDidLayoutSubviewsCallback = viewDidLayoutSubviewsCallback;
// (中略)
return (__bridge_retained void*)bridge;
}
// 生成したクラスの破棄
void iOSUtility_NativeEventListener_ReleaseUnityViewControllerListenerBridge(void* ptr)
{
UnityViewControllerListenerBridge* bridge = (__bridge_transfer UnityViewControllerListenerBridge*)ptr;
bridge.viewWillLayoutSubviewsCallback = nil;
bridge.viewDidLayoutSubviewsCallback = nil;
// (中略)
}
void iOSUtility_NativeEventListener_UnityRegisterViewControllerListener(void* ptr)
{
UnityViewControllerListenerBridge* bridge = (__bridge UnityViewControllerListenerBridge*)ptr;
// NOTE: UnityViewControllerListener.h にある登録用関数
UnityRegisterViewControllerListener(bridge);
}
void iOSUtility_NativeEventListener_UnityUnregisterViewControllerListener(void* ptr)
{
UnityViewControllerListenerBridge* bridge = (__bridge UnityViewControllerListenerBridge*)ptr;
UnityUnregisterViewControllerListener(bridge);
}
#ifdef __cplusplus
}
#endif
ちなみに iOS 向けのネイティブプラグインは Swift で実装することも可能ですが、本実装では ObjC++ を採用しています。
理由としては PluginBase 以下のヘッダが Swift から直接参照できず、Swift で対応する場合は UnityFramework.h(Umbrella Header)への変更が必要となるためです。
UnityFramework は Unity によって自動生成・更新される成果物であるため、保守性とアップデート耐性を考慮し、ObjC++ による実装を選択しています。
C# 側の実装
対応する C# 側の実装は次のようになります。
ポイントとしては、複数のインスタンスの登録に対応できるように、自身のインスタンスのポインタと interface を Dictionary で持っておき、呼び出しに応じて対象のインスタンスを指定できるようにしてあります。
internal sealed class UnityViewControllerListenerBridge : IDisposable
{
private static readonly Dictionary<IntPtr, IUnityViewControllerListener> Listeners = new();
private readonly IntPtr _ptr;
private bool _disposed;
public UnityViewControllerListenerBridge(IUnityViewControllerListener lifecycleListener)
{
var ptr = CreateUnityViewControllerListenerBridge(
ViewWillLayoutSubviewsCallbackStatic,
ViewDidLayoutSubviewsCallbackStatic,
// (中略)
);
Assert.IsNotNull(lifecycleListener);
Listeners[ptr] = lifecycleListener;
UnityRegisterViewControllerListener(ptr);
_ptr = ptr;
}
~UnityViewControllerListenerBridge()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!_disposed)
{
Assert.IsTrue(_ptr != IntPtr.Zero);
if (disposing)
{
// release managed resources
}
Listeners.Remove(_ptr);
UnityUnregisterViewControllerListener(_ptr);
ReleaseUnityViewControllerListenerBridge(_ptr);
_disposed = true;
}
}
[DllImport("__Internal", EntryPoint = "iOSUtility_NativeEventListener_CreateUnityViewControllerListenerBridge")]
private static extern IntPtr CreateUnityViewControllerListenerBridge(
ViewWillLayoutSubviewsCallback viewWillLayoutSubviewsCallback,
ViewDidLayoutSubviewsCallback viewDidLayoutSubviewsCallback,
// (中略)
);
[DllImport("__Internal", EntryPoint = "iOSUtility_NativeEventListener_ReleaseUnityViewControllerListenerBridge")]
private static extern void ReleaseUnityViewControllerListenerBridge(IntPtr ptr);
[DllImport("__Internal", EntryPoint = "iOSUtility_NativeEventListener_UnityRegisterViewControllerListener")]
private static extern void UnityRegisterViewControllerListener(IntPtr ptr);
[DllImport("__Internal", EntryPoint = "iOSUtility_NativeEventListener_UnityUnregisterViewControllerListener")]
private static extern void UnityUnregisterViewControllerListener(IntPtr ptr);
private delegate void ViewWillLayoutSubviewsCallback(IntPtr context);
private delegate void ViewDidLayoutSubviewsCallback(IntPtr context);
[MonoPInvokeCallback(typeof(ViewWillLayoutSubviewsCallback))]
private static void ViewWillLayoutSubviewsCallbackStatic(IntPtr context)
{
if (Listeners.TryGetValue(context, out var listenerInstance))
{
listenerInstance.OnViewWillLayoutSubviewsCallbacks();
}
}
[MonoPInvokeCallback(typeof(ViewDidLayoutSubviewsCallback))]
private static void ViewDidLayoutSubviewsCallbackStatic(IntPtr context)
{
if (Listeners.TryGetValue(context, out var listenerInstance))
{
listenerInstance.OnViewDidLayoutSubviewsCallbacks();
}
}
}
具体的な呼び出し例についてはサンプルプロジェクトの Assets/_Example/Scenes/SampleScene.unity シーンと関連コードをご覧ください。
おわりに
本記事では PluginBase 以下にある各種リスナーとそれをプラグインから活用する方法について解説しました。
とはいえ、以下に挙げるような幾つかのイベントは Unity 標準の機能からでも受け取ることが可能であり、特殊なケースを除けばあまり使わなくても済む場面の方が多いのかなとは思います。
ただ、プラグイン側で細かい制御などを行おうとすると、標準機能だけでは痒い所に手が届かないケースもあるかと思われるので、そういった場合に備えて参考にしていただけると幸いです。
おまけ: Unity 6.6 から入る変更について
ちょうど一ヶ月ぐらい前に開催された Unite 2025 Barcelona のロードマップ講演にて Unity と iOS のブリッジとなるレイヤーを刷新すると言うアナウンスがありました。
それに加えて Swift への移行を対応するほか、ライフサイクルやプラットフォームアクセスを容易にするための新しい API を追加するとの話がありました。
特に後者については今回解説したリスナー機能も関連してそうな気がするので、もし最新情報をキャッチアップできた際には随時更新していければと思います。
株式会社アプリボットでは、 エンジニアをはじめ全職種で積極採用中 です。
記事を読んで少しでも興味を持たれた方やお話を聞きたい方は、是非お気軽にご連絡ください!
-
Structure of a Unity Xcode project には記載されておらず、他にも Unity Discussions なども探ってみてもあまり情報が見つからなかった...。 ↩
-
名前に
PluginBaseと付いており、ある程度の拡張性を見込んだ機能かとは思われるので、大丈夫だとは思いたい所ですが...。 ↩ -
ちなみに Unity も内部的には
UIViewControllerを生成し、その中のUIViewを持つような構成となってます。 ↩ -
protocol とは C# で言うところの
interfaceに相当するもの。 ↩ -
上記のプロトコルには他にも
interfaceWillChangeOrientationとinterfaceDidChangeOrientationがありますが...関連する情報が見当たらなかった上に、どこで呼び出されているのかも不明だったため、説明中では取り扱ってません...。
↩ -
全部定義すると数が多いので、ここでは解説向けに
viewWillLayoutSubviews,viewDidLayoutSubviewsだけ定義してます。 ↩
