UnityでiOS(Swift 3)とAndroid(Java)のNative連携 - カメラロールから画像を取得し、Unity側で表示する

最近UnityでNative連携をしたのですが、結構はまったのでやり方を記しておきます。
既存の記事は古いものだったり、Swift3対応してなかったり、Unity側の関数をよんでなかったりしたので、一通り汎用でつかえそうなものとして残しておきます。
Native側の関数を呼び出すだけなら簡単なのですが、そこからデータを受け取ったりするのは結構やっかいです。

今回作成したアプリは[GitHub](https://github.com/wapa5pow/CameraRollSample)にあげてあります。

# 作りたいサンプルアプリ

題材として、カメラロールから画像を取得し、UnityのuGUIを使ってその画像を表示します。

作り終わったものは[githubに公開](https://github.com/wapa5pow/CameraRollSample)しています。

# 環境

* Unity: 5.5.1f1
* Xcode: 8.2.1
    * Swift: 3.0.2
* Android Studio: 2.3
    * Java: 1.8

# Unityアプリの作成

New Projectで`CameraRollSample`というプロジェクトを作成します。
この時点でのgithubは[unity-ui](https://github.com/wapa5pow/CameraRollSample/tree/unity-ui)というタグがついています。

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/079bf6b7-d5b9-f878-1aad-925c786180c3.png)

# iOS側の実装

注意1: ビルドしてうまくいかないときは、いっかいCleanしてからビルドしてください。
注意2: Unity側のC#のソースは実際にGitHub側から確認してください。

iOSのビルド設定にしてひとまずビルドしてみます。Build Identifierを適宜なおします。
ビルド先はBuilds/ios以下にします。なお、Xcode側でビルドした際に、Convert to Current Swift Syntaxとでるので、Convertとして先に進めます。

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/67119859-ffa9-3fd2-4183-e150713e3633.png)

正しくビルドされました。つぎにiOS側でカメラロールを呼び出すために以下のコードをPlugins/iOS以下に足してからビルドします。

`CameraRollViewController.swift`

```swift
import UIKit

public class CameraRollViewController: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
    
    static var filePath = "";
    
    public func open(_ path: String) {
        CameraRollViewController.filePath = path
        if UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.photoLibrary) {
            
            let picker = UIImagePickerController()
            picker.delegate = self
            picker.sourceType = UIImagePickerControllerSourceType.photoLibrary
            
            self.present(picker, animated: true, completion: nil)
        }
    }
    
    public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
        if let pickedImage = info[UIImagePickerControllerOriginalImage] as? UIImage {
            var resizedImage = fixOrientation(image: pickedImage)
            resizedImage = crop(image: resizedImage)
            resizedImage = resize(image: resizedImage, newSize: CGSize(width: 341, height: 341))
            let imageData = UIImagePNGRepresentation(resizedImage)
            let fileUrl = URL(fileURLWithPath: CameraRollViewController.filePath)
            do {
                try imageData!.write(to: fileUrl, options: .atomic)
                UnitySendMessage("GameController", "SetImage", CameraRollViewController.filePath);
            } catch {
                print(error)
            }
        }
        picker.dismiss(animated: true, completion: nil)
    }
    
    // 縦にとると横で保存されているのでオリエンテーションを修正する
    func fixOrientation(image: UIImage) -> UIImage {
        if (image.imageOrientation == UIImageOrientation.up) {
            return image;
        }
        
        UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale);
        let rect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
        image.draw(in: rect)

        let normalizedImage : UIImage = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext();
        return normalizedImage;
    }
    
    private func crop(image: UIImage) -> UIImage {
        let newCropLength: CGFloat = min(image.size.width, image.size.height)
        let x = image.size.width / 2.0 - newCropLength / 2.0;
        let y = image.size.height / 2.0 - newCropLength / 2.0;
        
        let cropRect = CGRect(x: x, y: y, width: newCropLength, height: newCropLength)
        let imageRef = image.cgImage!.cropping(to: cropRect)
        let cropped : UIImage = UIImage(cgImage: imageRef!, scale: 0, orientation: image.imageOrientation)
        
        return cropped
    }
    
    private func resize(image: UIImage, newSize: CGSize) -> UIImage {
        let widthRatio = newSize.width / image.size.width
        let heightRatio = newSize.height / image.size.height
        let ratio = (widthRatio < heightRatio) ? widthRatio : heightRatio
        let resizedSize = CGSize(width: (image.size.width * ratio), height: (image.size.height * ratio))
        UIGraphicsBeginImageContext(resizedSize)
        image.draw(in: CGRect(x: 0, y: 0, width: resizedSize.width, height: resizedSize.height))
        let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return resizedImage!
    }
}
```

`UnitySwift-Bridging-Header.h`

```c
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "UnityInterface.h"
```

`CameraRollViewControllerPlugin.mm`

```c
#import <CameraRollSample-Swift.h>

extern "C" {
    void OpenCameraRoll(const char *path) {
        CameraRollViewController *cameraRollViewController = [CameraRollViewController new];
        
        [UnityGetGLViewController() addChildViewController:cameraRollViewController];
        [cameraRollViewController open:[NSString stringWithUTF8String:path]];
    }
}
```

なお、上記のCameraRollSample-Swift.hは`Objective-C Generated Interface Header Name`の値にします。

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/f717b330-9720-737d-7688-38c82fe20072.png)

この時点でビルドすると、`.../CameraRollSample/Builds/ios/Libraries/Plugins/iOS/CameraRollViewController.swift:28:17: Use of unresolved identifier 'UnitySendMessage'`というエラーがでるので以下のように、設定する。

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/e41cdadd-298d-560f-da3f-9896907b2743.png)

以下のようにBridging Headerに`$(SRCROOT)/Libraries/Plugins/iOS/UnitySwift-Bridging-Header.h`を設定。

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/49ac12f8-508d-e25d-fa96-570f1d83e205.png)

つぎに、Runpath Search Pathsを設定します。`@executable_path/Frameworks`を入れます。

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/99a8b7d0-5829-5c35-5c18-b85e9d595a2a.png)

この時点でビルドはできますが、実行した時点で、Privacy設定がうまくいっていないのでボタンを押した際にクラッシュします。
`Info.plist`に`Privacy - Photo Library Usage Description`を以下のように追加します。文言は自由にしてください。

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/46fe9e95-5b85-9dc0-c1d5-eae4867e535e.png)


うまくできました。今回のコードは、`ios-camera-roll`というタグがついて、[GitHub](https://github.com/wapa5pow/CameraRollSample/tree/ios-camera-roll)にあげてあります。

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/8d03a48f-2c36-8958-cc3c-80fe28267bdb.png)

Xcode側で、Swiftのコードなどを修正して、Unity側にプラグインに反映させたいときは以下のようにコピーすればいいかなと思います。

```bash
cp Builds/ios/Libraries/Plugins/iOS/* Assets/Plugins/iOS/
```

# Android側の実装

注意: Unity側のC#のソースは実際にGitHub側から確認してください。

iOSのときは、ソースコードをUnityプロジェクトに配置すればいけましたが、Androidでは、AndroidStudioでプロジェクトをつくってそこで、AAR形式のアーカイブにしてからUnityに配置し、ビルドする形になります。

まずUnity側のビルド設定をAndroidにします。その後、ビルドして実際にAPKがAndroid端末に入るかどうか確かめます。なお、Minimum API Levelは14 (Android 4.0)にしました。この時点で統計によると、97.4%のようなので。

つぎに、AndroidStudioでプロジェクトを作ります。プロジェクトの作成場所は、任意ですが、今回は、Assetsと同じ位置に、AndroidStudioというフォルダ名で作成します。以下の設定以外はデフォルトでいきます。

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/89b430aa-77d1-6445-c136-77ef74703d33.png)

つぎに、AndroidStudio上でFile => New => Moduleで、Android Libraryを作ります。

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/6391df9d-44db-4c94-7a39-88fffb90b0ac.png)

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/3f8555a7-99ed-46b4-fc8b-3931f823d0f5.png)

以下のようにprojectができます。appはいらないのでFile => Project Structure でModulesのappをマイナスボタンで削除しておきます。

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/982b045f-545a-b05b-ad44-6784be647987.png)

つぎにUnityのclasses.jarをlibsフォルダに配置します。Unityのバージョンによっては元ファイルの場所が違うかもしれません。

```
cp /Applications/Unity/PlaybackEngines/AndroidPlayer/Variations/mono/Release/Classes/classes.jar AndroidStudio/plugin/libs/
```

`CameraRoll.java`と`CameraRollActivity.java`を以下のように配置します。ソースコードはGitHubを参考にしてください。

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/fee4a61d-d7b1-2056-0a69-c14d0e169d11.png)

`plugin/build.gradle`に以下を追加します。exportAarを実行すると、UnityのAssets/Plugins/Android以下にAndroidのjavaファイルをコンパイルしたAARファイルが配置されます。

```groovy
android.libraryVariants.all { variant ->
    variant.outputs.each { output ->
        output.packageLibrary.exclude('libs/classes.jar')
    }
}

task exportAar(type: Copy, dependsOn: assemble)  {
    from './build/outputs/aar'
    into '../../Assets/Plugins/Android'
    include 'plugin-release.aar'
}
```

ビルドしてAARファイルをつくって、Unity側に配置するために、以下をダブルクリックします。終わったら`Assets/Plugins/Android/plugin-release.aar`が配置されていることを確かめます。

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/6e2c64e7-d566-89de-5725-e147f633d393.png)

javaのコードで、Activityを新規追加したので、AndroidManifest.xmlに追加する必要があります。
`cp /Applications/Unity//PlaybackEngines/AndroidPlayer/Apk/AndroidManifest.xml Assets/Plugins/Android/`でUnityのデフォルトのAndroidManifest.xmlをコピーしたら以下のように追記します。

```xml
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.unity3d.player"
    xmlns:tools="http://schemas.android.com/tools"
    android:installLocation="preferExternal"
    android:versionCode="1"
    android:versionName="1.0">
    <supports-screens
        android:smallScreens="true"
        android:normalScreens="true"
        android:largeScreens="true"
        android:xlargeScreens="true"
        android:anyDensity="true"/>

    <application
        android:theme="@style/UnityThemeSelector"
        android:icon="@drawable/app_icon"
        android:label="@string/app_name"
        android:debuggable="true">
        <activity android:name="com.unity3d.player.UnityPlayerActivity"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <meta-data android:name="unityplayer.UnityActivity" android:value="true" />
        </activity>

        <!-- 追記部分 -->
        <activity android:name="com.wapa5pow.plugin.CameraRollActivity"
                  android:label="CameraRollActivity"
                  android:configChanges="keyboard|keyboardHidden|orientation">
        </activity>

    </application>
</manifest>
```

C#部分は、GitHubをみてください。ビルドして実機で試したら以下のように画像取得できました。
今回のコードは、`android-camera-roll`というタグがついて、[GitHub](https://github.com/wapa5pow/CameraRollSample/tree/android-camera-roll)にあげてあります。

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/68216efe-5cfb-c09a-dfcd-312e88d1f126.png)

# まとめ

UnityのNative Plugin開発で結構つまずいたのでまとめたものを記しました。
Unityのバージョンがあがったり、Swiftのバージョンがあがっても、できるだけ追従していきたいと思います。

# 参考
* http://qiita.com/mybdesign/items/fe3e390741799c1814ad