本編「【Unity】iOSネイティブプラグイン開発を完全に理解する」の付録記事です。
記事中での用語や略称についてはそのまま本編に倣う形で記載していきます。
ここでは私がネイティブプラグインを実装する際によくやる手法/補足について解説します。
あくまで一例として参考にして頂けると幸いです。
この記事の中では「Xcode上から普通にプロジェクトを作成して開発するiOS向けのアプリ」のことを便宜的にネイティブアプリ
として表記していきます。
TL;DR
-
iOS向けのネイティブプラグインを作るなら、新規でネイティブアプリ用のプロジェクトを立ち上げて、そこで開発/動作確認すると効率が良い
- 新規でネイティブアプリ用のプロジェクトを作る際には
UIKit
ベースがオススメ
- 新規でネイティブアプリ用のプロジェクトを作る際には
-
プラグイン向けに実装したネイティブコードの手直しはUnityのiOSビルド結果であるXcodeプロジェクト上からも可能
-
Symlink Unity libraries
の設定の有無に注意
-
-
プラグインとして導入するネイティブコードは別ソースに分けた上で独立性を高く実装
- Unityとネイティブアプリのどちらからでも呼び出しやすいように
プラグインとして使うネイティブコードはネイティブアプリのプロジェクト上で実装したほうが効率が良い
最終的にUnityに組み込む物と言えども、プラグインとして使うネイティブコードはネイティブアプリのプロジェクト上で実装したほうが圧倒的に効率が良いです。
→ Unityが一切関与しないので、「ビルドが通るかどうかのチェック」や「実機動作確認」を高速に行うことが可能。
具体的な流れとしては以下のようになります。
- Xcode上から新規でネイティブアプリ用のプロジェクトを立ち上げる
- 1で立ち上げたプロジェクト上からプラグインとして使うネイティブコードを実装 + 実機動作確認
- 2が完成したらソースをUnity上にインポート
実装/動作確認用のネイティブアプリは簡単なもので良い
ネイティブアプリのプロジェクト上で実装/動作確認を行うには、ある程度のネイティブアプリ開発に関する知見が必要となってきますが、先ずは 「ボタンを1つ配置してそこからプラグインとして利用するネイティブコードの処理を呼び出す」 と言った簡易的なものでも十分だと思います。
この記事中ではネイティブアプリ開発に関する初歩的なところまでは触れませんが、先ずは公式資料なり初心者向けの書籍/サイトなりを参照して「ボタンを一つ配置してそこから処理を呼び出す」ところまでをキャッチアップしてみることをオススメします。
【補足】 Storyboard
or SwiftUI
?
Xcodeから新規プロジェクト作成時に出てくる以下の画面にて、Interface
と言うドロップダウンを選択すると Storyboard
かSwiftUI
かのどちらかを選択する事になりますが、ここでは 「Storyboard」 を選択することをオススメします。
このStoryboard
と言うのはSwiftUI
が登場する以前からある従来の形式であり、更にそれらを包括する従来のUIフレームワーク全般のことをUIKit
と言うのですが、ザックリと言ってしまえば現時点のUnity iOSビルドで出力されるXcodeプロジェクトはその従来の形式(UIKit
)が使われているので、UI周りに関するプラグインを書く際には都合が良くなります。
この点についてはサンプルプロジェクトの解説にて再度補足します。
Unityにインポートしたネイティブコードの手直しはビルド結果のXcodeプロジェクト上からも可能
Unityにインポートしたネイティブコードはビルド結果のXcodeプロジェクトにそのままの形で含まれます。
→ 場所としてはLibraries
以下にAssets
以下のフォルダ構成そのままの形で入る。
(以下は本編中にあるMinimumExample
の例)
なので、もしネイティブコードの手直しが必要になったり、決め打ちの値を調整したくなったときにはUnityのビルド結果にあるネイティブコードを直接書き換えることで効率よくビルド/実機での動作確認を行えると言ったテクニックがあります。
【補足】 Symlink Unity libraries
についての補足
Build SettingsにあるSymlink Unity libraries
を有効にしてビルドすると、ビルド結果のXcodeプロジェクトに含まれるネイティブコードはシンボリックリンクとなるために、Xcodeで編集した際にはAssets以下にあるコードにもそのまま変更が適用されることになります。
逆にこちらのチェックをオフにした際にはネイティブコードはコピーされたものが含まれるので、Xcodeで編集した際にはAssets以下にあるコードにもそのまま適用されないので注意する必要があります。
その他、Symlink Unity libraries
の詳細については以下をご覧ください。
-
参考リンク
-
iOS build settings
- →
Symlink Unity libraries
を参照
- →
- iOSのビルド設定の「Symlink Unity libraries」の意味
-
iOS build settings
サンプルプロジェクト
ここまでの流れに加え、幾つかの要点を補足するためのサンプルを用意しました。
内容としては 「UnityのInputFieldから入力した文字列をネイティブのシェアUIを開いてシェアする」 アプリとなります。
サンプルは「objCpp/pluginDevelopment」ブランチにて管理しており、以下2点のプロジェクトを含んでます。
- Unityプロジェクト
- プラグイン開発用のネイティブアプリプロジェクト
- →
XcodeProjects/PluginDevelopment-Example/PluginDevelopment-Example.xcodeproj
- →
このサンプルのトピック
このサンプルでは主に以下のトピックについて話していきます。
-
プラグインとして導入するネイティブコードは独立性を高くして実装
- → Unity/ネイティブアプリの両方から呼び出しやすい設計に
UIKit
に関連するプラグインを実装する際の注意点
求める予備知識
あとはiOSネイティブアプリ開発周りの予備知識としては、以下の範囲まではキャッチアップ済みであるという事を前提に解説していきます。
-
Storyboard
&Objective-C
ベース1の新規プロジェクト作成- 新規ソースの追加
-
Storyboard
上にボタンを一つ配置して、押されたときの処理の呼び出しまで実装
■ プラグインとして導入するネイティブコードは独立性を高くして実装
プラグインとして導入するネイティブコードは別ソースに分けた上で、独立性を高く実装しておくと取り回しがよくなります。
早い話 「ネイティブアプリにベッタリ依存するような書き方は避けて剥がしやすくする」 と言うイメージですが、具体例について次で解説していきます。
※解説の対象となる該当ソース全体のリンクはこちら 2
■ Unity/ネイティブアプリの両方から呼び出しやすい設計に
もう少し具体的な内容に踏み込むと、シェアUIの表示を行うNativeShare
クラスは以下のような実装となっており、なるべくネイティブアプリの状態や呼び出し元のViewに依存しないような作りにしてあります。
今回主に依存性が発生しそうなポイントとしてはshareText
で呼び出されているUIActivityViewController
であり、こちらを表示する際には自身のUIViewController
を要求されますが、そちらを引数化することでUnity/ネイティブアプリの両方から呼び出しやすくしてます。
// MARK:- implementation (クラスの実装部)
@implementation NativeShare
+ (void)shareText:(NSString*)text viewController:(UIViewController*)vc {
// NOTE: UIActivityViewControllerでシェア
// ref: https://developer.apple.com/documentation/uikit/uiactivityviewcontroller?language=objc
NSArray* array = @[text];
UIActivityViewController* avc = [[UIActivityViewController alloc] initWithActivityItems:array applicationActivities:nil];
[vc presentViewController:avc animated:TRUE completion:nil];
}
@end
具体的な呼び出し箇所の実装及びポイントとしては以下のようになります。
ネイティブアプリからの呼び出し
ViewController
からボタン押下時にネイティブプラグイン向けのクラスとして実装しているNativeShare
のメソッドを直接呼び出すようにしてます。
ポイントとしては、C#から呼び出される想定のP/Invoke用の外部宣言関数はここでは呼び出しません。
ViewController
上からの呼び出しとなるので、shareText
の第二引数にはそのままself
を渡してます。
// ボタン押下で呼び出される処理
- (void)tapButton:(UIButton*)button {
// 渡す文字列
NSString* text = @"ネイティブアプリからの呼び出し";
// Unity向けのネイティブプラグインを呼び出し
[NativeShare shareText:text viewController:self];
}
ちなみに外部宣言関数の方は以下のように本来呼び出す想定のUnityGetGLViewController()
をコメントアウトし、代わりにダミー変数を渡すことでコンパイルだけ通るような形にしてます。
意図についてはコメントに記載してますが、改めて後述します。
#ifdef __cplusplus
extern "C" {
#endif
// NOTE: C#から渡される文字列はcharのポインタ型として渡される
void shareText(char* textPtr) {
// NSStringへの変換
NSString* text = [NSString stringWithCString:textPtr encoding:NSUTF8StringEncoding];
// NOTE: Unity上からViewControllerを取得するには`UnityGetGLViewController()`を使う
// ※これ自体はUnityがiOSビルドで出力したプロジェクト上からじゃないと呼び出せないので、ここではコンパイルを通すために一時的にコメントアウト
//UIViewController* vc = UnityGetGLViewController();
UIViewController* vc = nil;
[NativeShare shareText:text viewController:vc];
}
#ifdef __cplusplus
}
#endif
【補足】 ネイティブコードをUnityにインポートする際の変更点
上記のNativeShare
一式をUnityに持っていく際にはそのままのインポートはせずに、事前に以下の変更を加えててからインポートしてます。
- ヘッダーファイル(.h)をソースファイル(.mm)にマージ
- 必須ではないが、個人的にマージした方が1ソースで済むために管理しやすいからと言う理由でやっている
-
P/Invoke
で呼び出される外部宣言関数内にて、Unityが持つViewController
を渡すようにする- 詳細は次で解説
Unity側にインポートしたネイティブコード全体としては以下のようになります。
Unityからの呼び出し
Unityからの呼び出しは今まで通りにP/Invoke経由となるので、外部宣言している関数から呼び出されます。
ポイントとしては、Unity上からViewController
を取得するにはUnityGetGLViewController()
を呼び出すことで取得することが出来ます。
このUnityGetGLViewController()
はUnityがiOSビルドで出力するプロジェクト一式に含まれるUnityAppController.mm
と言うソースコードにて外部宣言されているので、呼び出すにはUnityのiOSビルド結果であることが前提3となります。
→ 上記のネイティブアプリ側でコメントアウトしているのはこの為であり、補足で記載している通り、ネイティブコードをUnityに持ってくる段階で変更を加えて呼び出せるようにします。
// NOTE: 実態はUnityがiOSビルドで出力するプロジェクト中に含まれる`UnityAppController.mm`と言うソースにある
extern UIViewController* UnityGetGLViewController();
// MARK:- extern "C" (Cリンケージで宣言)
#ifdef __cplusplus
extern "C" {
#endif
// NOTE: C#から渡される文字列はcharのポインタ型として渡される
void shareText(char* textPtr) {
// NSStringへの変換
NSString* text = [NSString stringWithCString:textPtr encoding:NSUTF8StringEncoding];
// NOTE: Unity上からViewControllerを取得するには`UnityGetGLViewController()`を使う
UIViewController* vc = UnityGetGLViewController();
[NativeShare shareText:text viewController:vc];
}
#ifdef __cplusplus
}
#endif
■ UIKit
に関連するプラグインを実装する際の注意点
今回例に出した「シェアUIの表示」と言った「UIに関する機能」を呼び出す際にはViewController
を要求されることがあります。
以下の補足にて 「新規でネイティブアプリのプロジェクトを立ち上げるならStoryboard
を選択した方が良い」 と書いた理由はこれであり、UnityがiOSビルドで出力するプロジェクトはUIKit
ベースとなるために、APIの親和性の観点から今はこちらを選択したほうが良いかと思います。4
ここまでのまとめ
サンプルを例に幾つかの要点を補足してきましたが、あくまで実装の一例として参考にして頂くようお願いします。
(ここに記載している手法が絶対ではない)
例えば以下の項目に記載している「ヘッダーファイル(.h)をソースファイル(.mm)にマージ」と言う対応は必須ではありませんし、UnityGetGLViewController
に関する変更はUIKit
が関わっているからこその変更内容になります。
→ 逆に言うとUIKit
が関わらないシンプルなAPIの呼び出しレベルであればこういった対応はしなくても良いかも
-
サンプルのネイティブアプリ側の言語がObjCなのは、ネイティブプラグインをObjCで実装する都合から合わせているだけです。ここらに関しては特に縛りはなく、「プラグインはObjCだけど、ネイティブアプリはSwiftベースで実装」みたいに実装しやすい方に合わせてもよいかと思います。 ↩
-
外部クラスから呼び出される都合から
NativeShare
クラスはソースとヘッダーに分ける形で用意してます ↩ -
ネイティブコード側で同名/相応の関数を実装してやればコンパイルを通すことはできるかもしれないが、今回のサンプルではそこまではやっていない。 ↩
-
一応は
UIKit
とSwiftUI
を連携するためのAPIも存在するが、その分キャッチアップに必要な範囲が広がってしまうので、必要になるまでは必須科目ではないかな〜と思ってます ↩