Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
6
Help us understand the problem. What is going on with this article?
@mao_

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

本編「【Unity】iOSネイティブプラグイン開発を完全に理解する」の付録記事です。
記事中での用語や略称についてはそのまま本編に倣う形で記載していきます。


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

この記事の中では「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と言うドロップダウンを選択するとStoryboardSwiftUIかのどちらかを選択する事になりますが、ここではStoryboardを選択することをオススメします。

スクリーンショット 2021-04-13 4.01.30.png

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

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

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

Unityにインポートしたネイティブコードはビルド結果のXcodeプロジェクトにそのままの形で含まれます。
→ 場所としてはLibraries以下にAssets以下のフォルダ構成そのままの形で入る。
(以下は本編中にあるMinimumExampleの例)

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

スクリーンショット 2021-04-19 3.06.49.png

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

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

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

スクリーンショット 2021-04-19 3.56.53.png

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

サンプルプロジェクト

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

サンプルは「plugin-development-objc++」ブランチにて管理しており、以下2点のプロジェクトを含んでます。

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

サンプルアプリのイメージはこちら (クリックで展開)

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

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

20210419_022245.GIF

Unityのサンプル

20210415_051328.GIF

このサンプルのトピック

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

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

求める予備知識

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

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

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

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

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

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

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

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

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を渡してます。

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

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

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

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

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側にインポートしたネイティブコード全体としては以下のようになります。

Unityからの呼び出し

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

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

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

/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

ここまでのまとめ

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

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


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

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

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

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

6
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
6
Help us understand the problem. What is going on with this article?