Unity 2019.3
から新機能として**「Unity as a Library」**が入った影響で、iOS向けビルド後の.xcodeproj
の構成に大きく変更が掛かりました。
その影響としてか、例えば以下の記事にある方法でSwiftコードを実装しようとしてもビルドが通らない状況となっていたので、今回は従来の方法と照らし合わせつつ解決策諸々を備忘録序に纏めていければと思います。
※以降、以下に記事にある2019.2までのやり方を従来のやり方
と表記する形で解説していきます。
ひょっとしたら「実はこのパターンが出来なかった」といった見落としがあるかもしれないので、そちらについては分かり次第随時追記予定。
(もし「ここの内容だとあれが出来ない」と言った情報があれば、コメントや編集リクエストなどで教えて頂けると幸いです )
バージョン
-
Unity
- 2018.4.30f1
- 従来のやり方の検証に利用
- 2019.4.17f1
- 2019.3からのやり方の検証に利用
- 2018.4.30f1
- Xcode 12.3
公開リポジトリ
今回検証に用いたプロジェクト一式をGitHubにて公開してます。
「従来のやり方を適用したプロジェクト」と「2019.3からのやり方を適用したプロジェクト」の2つを用意してます。
2019.3からどの様な変更が掛かったのか?
※一応補足として記載しておきます。「ここまでは知ってるので、早くSwiftコードを実装する方法を知りたい」という方は読み飛ばして下さい。
従来はUnity-iPhone
と言うターゲットに全てのコードやデータなどが集約される形となっておりましたが、2019.3からは新たに「UnityFramework」と言うフレームワークのターゲットが追加されており、Classes
やLibraries folders
、その他依存関係となっているフレームワーク各種はこちらに切り離されるようになりました。
ここらの詳細については公式ドキュメントの方にも記述があるので、詳しくは以下を御覧ください。
この変更によって大きく影響を受ける箇所としては、恐らくは[PostProcessBuild]
で処理されるであろうPBXProject
の更新処理であり、GUIDの向き先を以下の様にUnityFramework
に向け直す必要が出てきました。
[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));
#if UNITY_2019_3_OR_NEWER
// NOTE: 2019.3からは`UnityFramework`に設定を適用
var targetGUID = project.GetUnityFrameworkTargetGuid();
#else
var targetName = PBXProject.GetUnityTargetName();
var targetGUID = project.TargetGuidByName(targetName);
#endif
project.AddFrameworkToProject(targetGUID, "MonafuwaLowMemoryWarningFramework.framework", false);
File.WriteAllText(projectPath, project.WriteToString());
}
ただ、Swiftを扱う上では↑の様に単純にGUIDの向き先をUintyFramework
に変えるだけでは解決しないので、具体的な解決策を後述していきます。
従来のSwiftコードの実装方法
先に予備知識として2019.2までの従来の実装方法について記載していきます。
とは言え、やっている事自体はほぼ以下の記事にあるやり方そのままです。
こちらも大体把握されている方は読み飛ばしてしまっても問題有りません。
ここでは以下のプロジェクトにあるExsamplesを元に解説していきます。
PBXProjectの変更点
ほぼ記事にあるやり方のとおりです。
幾つか要点だけ箇条書きで纏めておきます。
-
Objective-C Bridging Header
にUnitySwift-Bridging-Header.h
を設定- ※
UnitySwift-Bridging-Header.h
はプロジェクト中に事前にプラグインとしてインポートされている物が設定されるように適用
- ※
-
Objective-C Generated Interface Header Name
にunityswift-Swift.h
を設定 -
Runpath Search Paths
に@executable_path/Frameworks
を設定 - Swiftのバージョンを
5.0
に設定- ※コメントにも記載しているが、こちらを明示的に指定しないとデフォルトで
3.0
辺りが設定される?影響で、XcodeによってはUnspecified
扱いになることがある - ※因みに
5.0
を指定しているのは最新だからとりあえず指定しているだけであって、特にこれと言った理由などは無い。こちらのバージョンは要件に応じて必要な値を設定すること
- ※コメントにも記載しているが、こちらを明示的に指定しないとデフォルトで
/// <summary>
/// Swiftを実装するにあたって必要な設定を自動で適用する
/// </summary>
sealed class XcodePostProcess
{
[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));
var targetGuid = project.TargetGuidByName(PBXProject.GetUnityTargetName());
project.SetBuildProperty(targetGuid, "SWIFT_OBJC_BRIDGING_HEADER", "Libraries/UnitySwift/UnitySwift-Bridging-Header.h");
project.SetBuildProperty(targetGuid, "SWIFT_OBJC_INTERFACE_HEADER_NAME", "unityswift-Swift.h");
project.AddBuildProperty(targetGuid, "LD_RUNPATH_SEARCH_PATHS", "@executable_path/Frameworks");
// Swift version: 5.0
// NOTE: 明示的に指定しないと3.0ぐらいの古いのが設定されるっぽいので、Xcodeによっては`Unspecified`扱いになる
project.AddBuildProperty(targetGuid, "SWIFT_VERSION", "5.0");
File.WriteAllText(projectPath, project.WriteToString());
}
}
UnitySwift-Bridging-Header.h
には予めFoundation.h
とUIKit.h
の他、UnityInterface.h
をimportしておきます。
(UnitySendMessage
などはUnityInterface.h
で宣言されているため、SwiftからUnitySendMessage
を呼び出したい場合にはimportしておく必要がある)
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "UnityInterface.h"
Swiftのコード
Swiftのコードは以下のように実装してます。
callSwiftMethod
はC#側から渡された文字列をprint
で出力するだけのものであり、callUnityMethod
はUnitySendMessage
を呼び出してC#側に"Hello, Unity!"と言う文字列を送ります。
import Foundation
class Example : NSObject {
// ObjC++からSwiftのメソッドを呼び出す
@objc static func callSwiftMethod(_ message: String) {
print("\(#function) is called with message: \(message)")
}
// SwiftのメソッドからSendMessageを呼び出す
@objc static func callUnityMethod() {
// Call a method on a specified GameObject.
UnitySendMessage("CallbackTarget", "OnCallFromSwift", "Hello, Unity!")
}
}
Objective-C++のコード
SwiftのコードだけだとC#から呼び出せないので、P/Invokeで呼び出せるようにObjC++コードを用意します。
内容はSwift側のメソッドをそのまま呼び出しているだけです。
#import <Foundation/Foundation.h>
// ObjC++からSwiftのクラスにアクセスする際に必要
// NOTE: `unityswift-Swift.h`の実態はビルド時に`DerivedData`以下に自動生成される
#import "unityswift-Swift.h" // Required
// P/Invoke
extern "C" {
void callSwiftMethod(const char *message) {
[Example callSwiftMethod:[NSString stringWithUTF8String:message]];
}
void callUnityMethod() {
[Example callUnityMethod];
}
}
C#のコード
上記で実装したSwiftコード及びP/Invoke用のObjC++コードの呼び出し処理を実装します。
Exsampleでは画面上にある2つのButtonからSwift側のcallSwiftMethod
とcallUnityMethod
を呼び出せるようにしてます。
なお、callUnityMethod
についてはSendMessageで結果を受け取る都合上、GameObject名やメソッド名を合わせる形にして受け取った文字列をログ出力するようにしてます。
**`Exsample.cs` (クリックで展開)**
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.UI;
namespace UnitySwift.Exsamples
{
/// <summary>
/// iOS NativePluginの呼び出しサンプル
/// </summary>
public sealed class Exsample : MonoBehaviour
{
[SerializeField] Button _callSwiftMethod = default;
[SerializeField] Button _callUnityMethod = default;
void Start()
{
// SwiftからSendMessageを呼び出す際に指定されているGameObjectの名称を設定
this.name = "CallbackTarget";
_callUnityMethod.onClick.AddListener(() =>
{
#if !UNITY_EDITOR && UNITY_IOS
CallUnityMethod();
#endif
});
_callSwiftMethod.onClick.AddListener(() =>
{
#if !UNITY_EDITOR && UNITY_IOS
CallSwiftMethod("Gorilla");
#endif
});
}
/// <summary>
/// SwiftからSendMessageで呼び出される措定のメソッド
/// </summary>
/// <param name="message"></param>
void OnCallFromSwift(string message)
{
Debug.Log(message);
}
/// <summary>
/// ObjC++からSwiftのメソッドを呼び出す
/// </summary>
/// <remarks>[C# -> ObjC++ -> Swift]の流れで呼び出される</remarks>>
[DllImport("__Internal", EntryPoint = "callSwiftMethod")]
static extern void CallSwiftMethod(string message);
/// <summary>
/// SwiftのメソッドからSendMessageを呼び出す
/// </summary>
/// <remarks>※SwiftからUnity側で定義されているメソッドを呼び出したい意図がある</remarks>>
[DllImport("__Internal", EntryPoint = "callUnityMethod")]
static extern void CallUnityMethod();
}
}
2019.3からのSwiftコードの実装方法
上述した従来のやり方のまま.xcodeproj
をビルドすると確実にエラーが出て怒られます。
その理由の一つとして、UnityFramework
の実態であるFrameworkと言うターゲットはBridging-Header.h
を設定することが出来ないので、恐らくは早いタイミングでこの旨に関連するエラーが出力されるかと思われます。
この他にも細かい所で幾つか変更が掛かっている箇所があるので、順を追って説明していきます。
ここでは以下のプロジェクトにあるExsamplesを元に解説していきます。
PBXProjectの変更点
先にPBXProject
の変更点についての解説ですが、結論から言うと**「Swiftのバージョン指定」以外不要になっているように思われました。**
UnitySwift-Bridging-Header.h
についてはそもそも設定することが出来ないので、プロジェクトからも削除してます。
sealed class XcodePostProcess
{
/// <summary>
/// Swiftを実装するにあたって必要な設定を自動で適用する
/// </summary>
[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();
// NOTE:
// 以前まで設定していた`Bridging-Header.h`の設定の類は2019.3からは不要な模様。
// 寧ろCocoa touch FrameworkがBridging-Headerに対応していないので、設定していると怒られる。
// Swift version: 5.0
// NOTE: 明示的に指定しないと3.0ぐらいの古いのが設定されるっぽいので、Xcodeによっては`Unspecified`扱いになる
project.AddBuildProperty(targetGuid, "SWIFT_VERSION", "5.0");
File.WriteAllText(projectPath, project.WriteToString());
}
}
Swiftのコード
変更点としては以下の2点が挙げられます。
- ObjC++に公開するクラス/メソッドのアクセスレベルを
public
に設定 UnitySendMessage
の代わりにUnityFramework
に実装されているsendMessageToGO
を利用
後者の変更については以下のissue/PRを参考にさせて頂きました。
(記憶だとsendMessageToGO
はUaaL1に於ける「Native → Unity」間のやりとり辺りで使われていた覚え)
従来のUnityInterface.h
はそもそもターゲットに含まれておらず、参照すら出来ない模様...?
import Foundation
// NOTE: ObjCに公開する物はアクセスレベルを`public` or `open`に設定する必要あり
public class Example : NSObject {
// ObjC++からSwiftのメソッドを呼び出す
@objc public static func callSwiftMethod(_ message: String) {
print("\(#function) is called with message: \(message)")
}
// SwiftのメソッドからSendMessageを呼び出す
@objc public static func callUnityMethod() {
// NOTE: 従来の`UnityInterface.h`にある`SendMessage`は参照でき無さそう?なので、以下のsendMessageToGOを利用する。
// Call a method on a specified GameObject.
if let uf = UnityFramework.getInstance() {
uf.sendMessageToGO(
withName: "CallbackTarget",
functionName: "OnCallFromSwift",
message: "Hello, Unity!")
}
}
}
Objective-C++のコード
こちらの主な変更点としては、importするヘッダーであり、対象をunityswift-Swift.h
からUnityFramework/UnityFramework-Swift.h
に変更してます。
#import <Foundation/Foundation.h>
// 2019.3からはこちらをimportする必要がある
#import <UnityFramework/UnityFramework-Swift.h>
// P/Invoke
extern "C" {
void callSwiftMethod(const char *message) {
[Example callSwiftMethod:[NSString stringWithUTF8String:message]];
}
void callUnityMethod() {
[Example callUnityMethod];
}
}
C#のコード
C#側の呼び出しコードに変更は無いので割愛。
参考リンク
- FrameworkでSwiftとObjective-C混ぜるのはやばい
- UnityのiOSネイティブプラグインをSwiftで書くためのネイティブプラグイン
- 【Unity】最近のUnityでC#からSwiftを実行する最短実装
- miyabi/unity-swift
-
UaaL・・・
Unity as a Library
の略称 ↩