「Swift５」SwiftでiOSプラグインをUnity向けに実装する方法

# はじめに
SwiftでiOSプラグインをUnity向けに実装する方法について音声読み上げアプリを開発しながらを解説していきます。
この記事はiOSプラグインをSwiftで実装する事にフォーカスしています。
詳細まで完璧に理解したい方は[こちら](https://qiita.com/mao_/items/5b33c90e533a538570b8)を閲覧してください。

# 開発環境
Xcode 13.4.1 
Swift5
# この記事の対象者
・Unityでネイティブの機能を実装したい方
・自作のiOSプラグインを作りたい方
・いろんな記事を見たがなぜかできない方<-元々は僕がそうでした
# プラグイン作成してUnityで使うまでの手順
１、Swiftで必要な処理をStaticLibraryで実装 (Xcode)
２、Swiftで書いた処理をObjC++で呼び出す (Xcode)
３、ObjC++をC#から呼び出せるようにする (Xcode)
４、C#から[P/Invoke](https://qiita.com/mao_/items/5b33c90e533a538570b8#pinvokeについて)で３、のObjC++で書いたメソッドを呼び出す (Unity)
# 前提知識
・Xcode（Swift）でiOSアプリを簡単なものであれば開発できる
・Unity（C#）でアプリ開発したことがある
# 順に解説
## １、Swiftで必要な処理をStaticLibraryで実装 (Xcode)
StaticLibraryでプロジェクトを作成
![スクリーンショット 2022-08-08 18.39.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654544/af1feeac-69bd-6651-2649-ed8d693390d7.png)
次に言語をObjective-Cを選択し、今回はプロジェクト名を仮に「staticLibrary」にしましょう。
![スクリーンショット 2022-08-08 18.46.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654544/fcd63304-e1d4-7657-753b-5db6d0072c80.png)
するとデフォルトでstaticLibrary.hファイルとstaticLibrary.mが生成される
![スクリーンショット 2022-08-08 18.52.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654544/b082ddac-adff-0f07-d596-3e80f010efd1.png)
### その外側にstaticLibrary_swift（音声読み上げ処理）クラスを作成(.swiftファイル)
デフォルトでstaticLibrary-Bridging-Headerファイルが作成されます
その後クラスやメソッドはpublicで記述してください


```swift
import AVFoundation
import Foundation
import UIKit
public class staticLibrary_swift : NSObject {

    public static var speechSynthesizer: AVSpeechSynthesizer!
    /// ログに"Hello World"と出力して2を返す。
    /// NOTE: ここではクラスメソッド(静的関数)として実装
    ///
    /// - Returns: 2固定
    @objc public static func SpeakMessage(message : String) -> Int32 {
        // ログ出力
        print("音声読み上げ開始")
        print(message)
        // AVSpeechSynthesizerのインスタンス作成
        speechSynthesizer = AVSpeechSynthesizer()
        // 読み上げる、文字、言語などの設定
        let utterance = AVSpeechUtterance(string: message) // 読み上げる文字
        utterance.voice = AVSpeechSynthesisVoice(language: "ja-JP") // 言語 英語は en-US
        utterance.rate = 0.5 // 読み上げ速度
        utterance.pitchMultiplier = 1.0 // 読み上げる声のピッチ
        utterance.preUtteranceDelay = 0.2 // 読み上げるまでのため
        speechSynthesizer.speak(utterance)
        
        // 戻り値を返す
        return 2
    }
}

```
## ２、Swiftで書いた処理をObjC++で呼び出す (Xcode)
### Objective-CファイルをObjective-C++ファイルに変更する
このプロジェクトを作ったときに、自動で生成されたObjective-Cの「staticLibrary.m」のファイル名を「staticLibrary.mm」に変更する
→これだけでXocdeが自動でObjective-C++ファイルと認識してくれる
### staticLibrary.mmのファイルからSpeakMessageメソッドを呼び出す
今回はstaticLibrary.hファイルの中身をstaticLibrary.mmにまとめて書きます
#### 元々staticLibrary.hファイルに記述していた内容

```swift
#import <Foundation/Foundation.h>

@interface staticLibrary : NSObject
// ログに"Hello World"と出力して2を返す。
// NOTE: ここではクラスメソッド(静的関数)として実装
+ (int32_t)SpeakMessage;

@end

```
ここではSpeakMessageメソッドを宣言している
#### staticLibrary.mm(ObjC++)でSpeakMessageメソッド(Swift)を呼び出す
```swift
// NOTE: この関数が実際にUnity(C#)から呼び出される
int32_t SpeakMessage(const char *message) {
     // 上記で宣言・実装した`Example.printHelloWorld`を呼び出す。呼び出し時の構文はObjC形式となる。
     // NOTE: クラスメソッド(静的関数)として実装しているので、クラスをインスタンス化せずに直接呼び出せる
     return [staticLibrary_swift SpeakMessageWithMessage:@(message)];
}
```
SpeakMessageメソッドを呼び出す
## ３、ObjC++をC#から呼び出せるようにする (Xcode)
staticLibrary.mmファイルの全コード
```swift
//staticLibrary.hファイルの内容
#import <Foundation/Foundation.h>

@interface staticLibrary : NSObject
// ログに"Hello World"と出力して2を返す。
// NOTE: ここではクラスメソッド(静的関数)として実装
+ (int32_t)SpeakMessage;

@end
// 2019.3からはこちらをimportする必要がある
#import <UnityFramework/UnityFramework-Swift.h>

// MARK:- extern "C" (Cリンケージで宣言)
extern "C" {
    // NOTE: この関数が実際にUnity(C#)から呼び出される
    int32_t SpeakMessage(const char *message) {
    
         // 上記で宣言・実装した`Example.printHelloWorld`を呼び出す。呼び出し時の構文はObjC形式となる。
         // NOTE: クラスメソッド(静的関数)として実装しているので、クラスをインスタンス化せずに直接呼び出せる
         return [staticLibrary_swift SpeakMessageWithMessage:@(message)];
    }
}
```
extern "C"{}：外部（C#等）からのアクセスを可能にする
#import < UnityFramework/UnityFramework-Swift.h > :
 2019.3からはこちらをimportする必要があります。
またUnityからXcodeにiOSビルドするまではエラーが出るが気にしないで大丈夫です。
## ４、C#からP/Invokeで３、のObjC++で書いたメソッドを呼び出す (Unity)
### ①Unityプロジェクトの作成
```
//[]はフォルダ
[Assets]
      L [Plugins]
            L [iOS]
                L staticLibrary.mm
　　　　　　　　     staticLibrary_swift.swift
      L [Scenes]
            L XcodePostProcess.cs
      L [Scripts]
　　　　　　　L [Editor]
          　L getStaticLibrary.cs
```
上記のフォルダ構成でプロジェクトを作成します。
PluginsフォルダのiOSフォルダの中にstaticLibrary.mmとstaticLibrary_swift.swiftをドラッグ&ドロップで入れる

### ②XcodePostProcess.csでSwiftのバージョン指定を自動化->PostProcessBuild 
下記のコードはUnityからXcodeにビルドする際にSwift5を指定を自動化

```c#
using System.IO;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;
using UnityEngine;



public class XcodePostProcess : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
    [PostProcessBuild]
    static void OnPostProcessBuild(BuildTarget target, string path)
    {
        if (target != BuildTarget.iOS) return;
        var projectPath = PBXProject.GetPBXProjectPath(path);
        var project = new PBXProject();
        project.ReadFromString(File.ReadAllText(projectPath));

        // 2019.3からは`UnityFramework`に分離しているので、targetGuidはこちらを指定する必要がある。
        // NOTE: 前バージョンと共存させたい場合には「#if UNITY_2019_3_OR_NEWER」で分けることも可能
        var targetGuid = project.GetUnityFrameworkTargetGuid();

        // Swift version: 5.0
        // 
        // NOTE:
        // Unityのバージョンによっては?生成されるxcodeprojに古いSwiftのバージョンが指定されてるせいで、
        // 開くXcodeが新しかったりすると`Unspecified`扱いになるので一応は明示的に指定しておく
        project.SetBuildProperty(targetGuid, "SWIFT_VERSION", "5.0");

        File.WriteAllText(projectPath, project.WriteToString());
    }
}


```
### ③P/InvokeでXcodeで作成したStaticLibraryを呼び出す
getStaticLibrary.csファイルからstaticLibrary.mmファイルのSpeakMessageメソッドを呼び出す
下記がstaticLibrary.mmファイル全体のコードです

```c#
using System;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.UI;

namespace MinimumExample
{
  
    sealed class getStaticLibrary : MonoBehaviour
    {
        string message = "";
        [SerializeField] Button _button = default;
        [SerializeField] InputField _InputField = default;

        void Start()
        {
            _button.onClick.AddListener(() =>
            {
                //入力された文字をmessageに 
                message = _InputField.text;

                // プラグインの呼び出し
                var ret = SpeakMessage(message);
                Debug.Log($"戻り値: {ret}");

            });
        }

        #region P/Invoke

        // ObjectiveC++コードで実装した`Example`クラスのP/Invoke
        /// NOTE: Example.mmの「extern "C"」内で宣言した関数をここで呼び出す
        /// - iOSのプラグインは静的に実行ファイルにリンクされるので、`DllImport`にはライブラリ名として「__Internal」を指定する必要がある
        /// - `EntryPoint`に.mm側で宣言されている名前を渡すことでC#側のメソッド名は別名を指定可能
        [DllImport("__Internal", EntryPoint = "SpeakMessage")]
        static extern Int32 SpeakMessage(string message);


        #endregion P/Invoke

    }
}

```
下記のコードでUnityにStaticLibraryのSpeakMessageメソッドを登録
```c#
#region P/Invoke
     [DllImport("__Internal", EntryPoint = "SpeakMessage")]
     static extern Int32 SpeakMessage(string message);
#endregion P/Invoke
```
下記のコードでSpeakMessageメソッドを呼び出している
```c#
// プラグインの呼び出し
     var ret = SpeakMessage(message);
     Debug.Log($"戻り値: {ret}");
```
# おわりに
最後まで読んでいただきありがとうございます。
今まで自作プラグインを実装した事がなかったので、たくさんの記事を参考にしてやっと理解しました。
今回は自分が苦労して理解したので、その忘備録として記事を書きました。
参考にした記事
[iOSネイティブプラグイン開発を完全に理解する](https://qiita.com/mao_/items/5b33c90e533a538570b8)
[UnityでSwiftの自作「Static Library」を使う](https://qiita.com/kumatta_ss/items/15152207cb05e54ad793#)