【Unity】iOSネイティブプラグイン開発を完全に理解する - ネイティブプラグインを実装するには


本編「[【Unity】iOSネイティブプラグイン開発を完全に理解する](https://qiita.com/mao_/items/5b33c90e533a538570b8)」の付録記事です。
[記事中での用語や略称](https://qiita.com/mao_/items/5b33c90e533a538570b8#%E8%A8%98%E4%BA%8B%E4%B8%AD%E3%81%A7%E8%A7%A3%E8%AA%AC%E3%81%99%E3%82%8B%E8%A8%80%E8%AA%9E%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)についてはそのまま本編に倣う形で記載していきます。

--------------------------------------------

ここでは私がネイティブプラグインを実装する際によくやる手法/補足について解説します。
あくまで一例として参考にして頂けると幸いです。

この記事の中では「Xcode上から普通にプロジェクトを作成して開発するiOS向けのアプリ」のことを便宜的に`ネイティブアプリ`として表記していきます。


# TL;DR

- **iOS向けのネイティブプラグインを作るなら、新規でネイティブアプリ用のプロジェクトを立ち上げて、そこで開発/動作確認すると効率が良い**
    - 新規でネイティブアプリ用のプロジェクトを作る際には`UIKit`ベースがオススメ
- **プラグイン向けに実装したネイティブコードの手直しはUnityのiOSビルド結果であるXcodeプロジェクト上からも可能**
    - `Symlink Unity libraries`の設定の有無に注意
- **プラグインとして導入するネイティブコードは別ソースに分けた上で独立性を高く実装**
    - Unityとネイティブアプリのどちらからでも呼び出しやすいように


# プラグインとして使うネイティブコードはネイティブアプリのプロジェクト上で実装したほうが効率が良い

最終的にUnityに組み込む物と言えども、**プラグインとして使うネイティブコードはネイティブアプリのプロジェクト上で実装したほうが圧倒的に効率が良いです。**
→ Unityが一切関与しないので、「ビルドが通るかどうかのチェック」や「実機動作確認」を高速に行うことが可能。

具体的な流れとしては以下のようになります。

1. Xcode上から新規でネイティブアプリ用のプロジェクトを立ち上げる
2. 1で立ち上げたプロジェクト上からプラグインとして使うネイティブコードを実装 + 実機動作確認
3. 2が完成したらソースをUnity上にインポート



## 実装/動作確認用のネイティブアプリは簡単なもので良い

ネイティブアプリのプロジェクト上で実装/動作確認を行うには、ある程度の**ネイティブアプリ開発に関する知見**が必要となってきますが、先ずは **「ボタンを1つ配置してそこからプラグインとして利用するネイティブコードの処理を呼び出す」** と言った簡易的なものでも十分だと思います。

この記事中ではネイティブアプリ開発に関する初歩的なところまでは触れませんが、先ずは公式資料なり初心者向けの書籍/サイトなりを参照して「ボタンを一つ配置してそこから処理を呼び出す」ところまでをキャッチアップしてみることをオススメします。


### 【補足】 `Storyboard` or `SwiftUI`？

Xcodeから新規プロジェクト作成時に出てくる以下の画面にて、`Interface`と言うドロップダウンを選択すると **`Storyboard`か`SwiftUI`かのどちらか**を選択する事になりますが、<font color="Red">ここでは **「Storyboard」** を選択することをオススメします。</font>

![スクリーンショット 2021-04-13 4.01.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/854f944e-bf15-89be-a5ae-c6d68e96e8f8.png)

この`Storyboard`と言うのは`SwiftUI`が登場する以前からある従来の形式であり、**更にそれらを包括する従来のUIフレームワーク全般のことを`UIKit`と言う**のですが、ザックリと言ってしまえば現時点のUnity iOSビルドで出力されるXcodeプロジェクトは**その従来の形式(`UIKit`)が使われているので、UI周りに関するプラグインを書く際には都合が良くなります。**

この点については[サンプルプロジェクトの解説](#サンプルプロジェクト)にて再度補足します。






## Unityにインポートしたネイティブコードの手直しはビルド結果のXcodeプロジェクト上からも可能

Unityにインポートしたネイティブコードは**ビルド結果のXcodeプロジェクトにそのままの形で含まれます。**
→ 場所としては`Libraries`以下に`Assets`以下のフォルダ構成そのままの形で入る。
(以下は[本編中](https://qiita.com/mao_/items/5b33c90e533a538570b8#%E3%81%93%E3%81%AE%E7%AB%A0%E3%81%AE%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88)にある`MinimumExample`の例)

なので、**もしネイティブコードの手直しが必要になったり、決め打ちの値を調整したくなったときにはUnityのビルド結果にあるネイティブコードを直接書き換えることで効率よくビルド/実機での動作確認を行えると言ったテクニックがあります。**

![スクリーンショット 2021-04-19 3.06.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/44395464-502e-59c5-8d24-bc492e31c5a5.png)


### 【補足】 `Symlink Unity libraries`についての補足

Build Settingsにある`Symlink Unity libraries`を有効にしてビルドすると、ビルド結果のXcodeプロジェクトに含まれるネイティブコードはシンボリックリンクとなるために、**Xcodeで編集した際にはAssets以下にあるコードにもそのまま変更が適用されることになります。**

逆にこちらのチェックをオフにした際にはネイティブコードはコピーされたものが含まれるので、**Xcodeで編集した際にはAssets以下にあるコードにもそのまま適用されないので注意する必要があります。**

![スクリーンショット 2021-04-19 3.56.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/6203df2b-26c3-6f4d-b611-a1905a8e35f0.png)

その他、`Symlink Unity libraries`の詳細については以下をご覧ください。

- **参考リンク**
    - [iOS build settings](https://docs.unity3d.com/Manual/BuildSettingsiOS.html)
        - → `Symlink Unity libraries`を参照
    - [iOSのビルド設定の「Symlink Unity libraries」の意味](https://nakamura001.hatenablog.com/entry/20120906/1346937762)



# サンプルプロジェクト

ここまでの流れに加え、幾つかの要点を補足するためのサンプルを用意しました。
内容としては **「UnityのInputFieldから入力した文字列をネイティブのシェアUIを開いてシェアする」** アプリとなります。

サンプルは「[objCpp/pluginDevelopment](https://github.com/mao-test-h/UnityNativePlugin-for-iOS-Examples/tree/objCpp/pluginDevelopment)」ブランチにて管理しており、以下2点のプロジェクトを含んでます。

- Unityプロジェクト
- プラグイン開発用のネイティブアプリプロジェクト
    - → `XcodeProjects/PluginDevelopment-Example/PluginDevelopment-Example.xcodeproj`

<details><summary><b>サンプルアプリのイメージはこちら (クリックで展開)<b></summary><div>

**ネイティブアプリのサンプル**

※ネイティブアプリ側はあくまで動作確認用なのでシェアUIに渡す文字列は固定です。

![20210419_022245.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/97f9a3de-2574-b748-415e-63c3cffbbe8a.gif)

**Unityのサンプル**

![20210415_051328.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80207/a36d0773-5aa8-0099-08a5-ee48c2bcd255.gif)

</div></details>


## このサンプルのトピック

このサンプルでは主に以下のトピックについて話していきます。

- **プラグインとして導入するネイティブコードは独立性を高くして実装**
    - **→ Unity/ネイティブアプリの両方から呼び出しやすい設計に**
- **`UIKit`に関連するプラグインを実装する際の注意点**


## 求める予備知識

あとはiOSネイティブアプリ開発周りの予備知識としては、以下の範囲まではキャッチアップ済みであるという事を前提に解説していきます。

- `Storyboard` & `Objective-C`ベース[^1]の新規プロジェクト作成
    - 新規ソースの追加
- `Storyboard`上にボタンを一つ配置して、押されたときの処理の呼び出しまで実装


[^1]: サンプルのネイティブアプリ側の言語がObjCなのは、ネイティブプラグインをObjCで実装する都合から合わせているだけです。ここらに関しては特に縛りはなく、「プラグインはObjCだけど、ネイティブアプリはSwiftベースで実装」みたいに実装しやすい方に合わせてもよいかと思います。



## ■ プラグインとして導入するネイティブコードは独立性を高くして実装

プラグインとして導入するネイティブコードは別ソースに分けた上で、独立性を高く実装しておくと取り回しがよくなります。
早い話 **「ネイティブアプリにベッタリ依存するような書き方は避けて剥がしやすくする」** と言うイメージですが、具体例について次で解説していきます。

※解説の対象となる該当ソース全体のリンクはこちら [^2]

[^2]: 外部クラスから呼び出される都合から`NativeShare`クラスはソースとヘッダーに分ける形で用意してます

- [NativeShare.mm](https://github.com/mao-test-h/UnityNativePlugin-for-iOS-Examples/blob/objCpp/pluginDevelopment/XcodeProjects/PluginDevelopment-Example/PluginDevelopment-Example/NativePlugins/NativeShare.mm)
- [NativeShare.h](https://github.com/mao-test-h/UnityNativePlugin-for-iOS-Examples/blob/objCpp/pluginDevelopment/XcodeProjects/PluginDevelopment-Example/PluginDevelopment-Example/NativePlugins/NativeShare.h)



## ■ Unity/ネイティブアプリの両方から呼び出しやすい設計に

もう少し具体的な内容に踏み込むと、シェアUIの表示を行う`NativeShare`クラスは以下のような実装となっており、**なるべくネイティブアプリの状態や呼び出し元のViewに依存しないような作りにしてあります。** 

今回主に依存性が発生しそうなポイントとしては`shareText`で呼び出されている`UIActivityViewController`であり、こちらを表示する際には自身の`UIViewController`を要求されますが、そちらを引数化することでUnity/ネイティブアプリの両方から呼び出しやすくしてます。

```objc++:NativeShare.mm
// 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`を渡してます。

```objc:XcodeProjects/PluginDevelopment-Example/PluginDevelopment-Example/ViewController.m
// ボタン押下で呼び出される処理
- (void)tapButton:(UIButton*)button {

    // 渡す文字列
    NSString* text = @"ネイティブアプリからの呼び出し";

    // Unity向けのネイティブプラグインを呼び出し
    [NativeShare shareText:text viewController:self];
}
```

ちなみに外部宣言関数の方は以下のように**本来呼び出す想定の`UnityGetGLViewController()`をコメントアウトし、代わりにダミー変数を渡すことでコンパイルだけ通るような形にしてます。**
意図についてはコメントに記載してますが、改めて後述します。

```objc++:XcodeProjects/PluginDevelopment-Example/PluginDevelopment-Example/ViewController.m
#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側にインポートしたネイティブコード全体としては以下のようになります。

- [NativeShare.mm](https://github.com/mao-test-h/UnityNativePlugin-for-iOS-Examples/blob/objCpp/pluginDevelopment/Assets/PluginDevelopmentExample/Plugins/iOS/NativeShare.mm)





### Unityからの呼び出し

Unityからの呼び出しは今まで通りにP/Invoke経由となるので、外部宣言している関数から呼び出されます。

**ポイントとしては、Unity上から`ViewController`を取得するには`UnityGetGLViewController()`を呼び出すことで取得することが出来ます。**

この`UnityGetGLViewController()`はUnityがiOSビルドで出力するプロジェクト一式に含まれる`UnityAppController.mm`と言うソースコードにて外部宣言されているので、**呼び出すにはUnityのiOSビルド結果であることが前提[^3]となります。**
→ 上記のネイティブアプリ側でコメントアウトしているのはこの為であり、補足で記載している通り、ネイティブコードをUnityに持ってくる段階で変更を加えて呼び出せるようにします。

[^3]: ネイティブコード側で同名/相応の関数を実装してやればコンパイルを通すことはできるかもしれないが、今回のサンプルではそこまではやっていない。

```objc++:/Assets/PluginDevelopmentExample/Plugins/iOS/NativeShare.mm
// 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]

- [【補足】 Storyboard or SwiftUI？](#補足-storyboard-or-swiftui)

[^4]: 一応は`UIKit`と`SwiftUI`を連携するためのAPIも存在するが、その分キャッチアップに必要な範囲が広がってしまうので、必要になるまでは必須科目ではないかな〜と思ってます



## ここまでのまとめ

サンプルを例に幾つかの要点を補足してきましたが、あくまで実装の一例として参考にして頂くようお願いします。
(ここに記載している手法が絶対ではない)

例えば以下の項目に記載している「ヘッダーファイル(.h)をソースファイル(.mm)にマージ」と言う対応は必須ではありませんし、**`UnityGetGLViewController`に関する変更は`UIKit`が関わっているからこその変更内容になります。**
→ 逆に言うと`UIKit`が関わらないシンプルなAPIの呼び出しレベルであればこういった対応はしなくても良いかも

- [【補足】 ネイティブコードをUnityにインポートする際の変更点](#補足-ネイティブコードをunityにインポートする際の変更点)