Unity as a Library×Swiftの記事はシリーズになっています。
記事を順番に読み進めると、Unity as a LibraryをSwiftで使えるようになります。
- Unity as a LibraryでXCFrameworkを生成する方法(Swift) ←イマココ
- Unity as a LibraryのfXCFrameworkを生成するGitHub Actions
はじめに
本記事は どすこい塾 Advent Calendar 2025 の14日目の記事です。
Unity as a LibraryでXCFrameworkを生成する方法を紹介します。
環境
- OS:macOS Sequoia 15.6(24G84)
- Swift:6.2.1
「Unity as a Library」とは?
名前の通り、Unityをライブラリとして扱うことです。
AndroidやiOSに加え、Windowsをサポートしています。
iOSではUnity as a LibraryでUnityのライブラリをXcodeプロジェクトとして出力し、それをXCFrameworkに変換して使うのが一般的で、本記事でもそのように扱います。
XCFrameworkの生成手順
実際にXCFrameworkを生成する手順を紹介します。
前提条件
最初に前提条件を記載します。
- 以下の記事が素晴らしく、基本的にはこちらを見れば問題ない
- https://qiita.com/mao_/items/9874c1efa280ed4bb399
- 本記事ではアレンジした箇所や、説明されていない箇所に絞って説明する
- シミュレータ用のXcodeプロジェクトは、空のプロジェクトを使う
- Xcode上で新規作成し、デプロイメントターゲットのみ調整した
-
UnityProject/iosEmptyBuildにUnityFramework.xcodeprojという名前で作成した
.gitignore の作成
Gitで管理しないファイルを .gitignore に記述します。
+ *.xcarchive
+ *.xcframework
+ xcuserdata/
+ /UnityProject/iosBuild/build
+ /UnityProject/iosEmptyBuild/build
+ .DS_Store
Xcodeプロジェクトの生成
Unity上で実行し、実機用のXcodeプロジェクトを生成します。
名前は Unity-iPhone.xcodeproj となっているはずです。
詳細な手順は省略します。
Xcodeプロジェクトの更新
以下の公式ドキュメントにある 6. Make Data folder to be part of the UnityFramework を行います。
5. Expose NativeCallProxy.h はUnity側からSwift側の処理を実行したいのみ行います。
以下の記事の「2-3. DisplayManager.mm を修正」にある通り、 DisplayManager.mm も更新します。
差分を確認します。
5. Expose NativeCallProxy.h
- XXXXXXXXXXXXXXXXXXXXXXXX /* NativeCallProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = XXXXXXXXXXXXXXXXXXXXXXXX /* NativeCallProxy.h */; };
+ XXXXXXXXXXXXXXXXXXXXXXXX /* NativeCallProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = XXXXXXXXXXXXXXXXXXXXXXXX /* NativeCallProxy.h */; settings = {ATTRIBUTES = (Public, ); }; };
6. Make Data folder to be part of the UnityFramework
XXXXXXXXXXXXXXXXXXXXXXXX /* AppDelegateListener.mm in Sources */ = {isa = PBXBuildFile; fileRef = XXXXXXXXXXXXXXXXXXXXXXXX /* AppDelegateListener.mm */; };
+ XXXXXXXXXXXXXXXXXXXXXXXX /* Data in Resources */ = {isa = PBXBuildFile; fileRef = XXXXXXXXXXXXXXXXXXXXXXXX /* Data */; };
XXXXXXXXXXXXXXXXXXXXXXXX /* TransactionUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = XXXXXXXXXXXXXXXXXXXXXXXX /* TransactionUseCase.swift */; };
files = (
+ XXXXXXXXXXXXXXXXXXXXXXXX /* Data in Resources */,
XXXXXXXXXXXXXXXXXXXXXXXX /* PrivacyInfo.xcprivacy in Resources */,
);
@autoreleasepool
{
id ufw = UnityFrameworkLoad();
+ [ufw setDataBundleId: "com.unity3d.framework"];
[ufw runUIApplicationMainWithArgc: argc argv: argv];
return 0;
}
「2-3. DisplayManager.mm を修正」
surface->memorylessDepth = params.metalMemorylessDepth;
const int api = UnitySelectedRenderingAPI();
- if (api == apiMetal)
+ if (api == apiMetal && params.renderW != 0 && params.renderH != 0)
((UnityDisplaySurfaceMTL*)surface)->framebufferOnly = params.metalFramebufferOnly;
if (recreateSystemSurface)
毎回手動で更新するのが手間なのでスクリプト化します。
XXXXXXXXXXXXXXXXXXXXXXXX の部分は各自差し替えてください。
.PHONY: fix-codes
fix-codes:
$(MAKE) fix-pbxproj
$(MAKE) fix-main
$(MAKE) fix-display-manager
# ref: https://github.com/Unity-Technologies/uaal-example/blob/e44eadf2979032274b02dc9357964097871ce9c5/docs/ios.md
.PHONY: fix-pbxproj
fix-pbxproj:
if ! grep -qF 'XXXXXXXXXXXXXXXXXXXXXXXX /* Data in Resources */ = {isa = PBXBuildFile; fileRef = XXXXXXXXXXXXXXXXXXXXXXXX /* Data */; };' './UnityProject/iosBuild/Unity-iPhone.xcodeproj/project.pbxproj'; then \
sed -i '' -e 's/\(XXXXXXXXXXXXXXXXXXXXXXXX \/\* AppDelegateListener\.mm in Sources \*\/ = {isa = PBXBuildFile; fileRef = XXXXXXXXXXXXXXXXXXXXXXXX \/\* AppDelegateListener\.mm \*\/; };\)/\1\
\n\t\tXXXXXXXXXXXXXXXXXXXXXXXX \/\* Data in Resources \*\/ = {isa = PBXBuildFile; fileRef = XXXXXXXXXXXXXXXXXXXXXXXX \/\* Data \*\/; };/g' ./UnityProject/iosBuild/Unity-iPhone.xcodeproj/project.pbxproj; \
fi
if ! grep -q ' XXXXXXXXXXXXXXXXXXXXXXXX /\* Data in Resources \*/,' './UnityProject/iosBuild/Unity-iPhone.xcodeproj/project.pbxproj'; then \
sed -i '' 's/\(\t\t\t\t\)\(XXXXXXXXXXXXXXXXXXXXXXXX \/\* PrivacyInfo\.xcprivacy in Resources \*\/,\)/\1XXXXXXXXXXXXXXXXXXXXXXXX \/\* Data in Resources \*\/,\
\n\1\2/' ./UnityProject/iosBuild/Unity-iPhone.xcodeproj/project.pbxproj; \
fi
sed -i '' 's/\(XXXXXXXXXXXXXXXXXXXXXXXX \/\* NativeCallProxy\.h in Headers \*\/ = {isa = PBXBuildFile; fileRef = XXXXXXXXXXXXXXXXXXXXXXXX \/\* NativeCallProxy\.h \*\/;\) };/\1 settings = {ATTRIBUTES = (Public, ); }; };/' ./UnityProject/iosBuild/Unity-iPhone.xcodeproj/project.pbxproj
# ref: https://github.com/Unity-Technologies/uaal-example/blob/e44eadf2979032274b02dc9357964097871ce9c5/docs/ios.md
.PHONY: fix-main
fix-main:
if ! grep -q '\[ufw setDataBundleId: "com\.unity3d\.framework"\];' './UnityProject/iosBuild/MainApp/main.mm'; then \
sed -i '' -e 's/ id ufw = UnityFrameworkLoad();/ id ufw = UnityFrameworkLoad();\n [ufw setDataBundleId: "com.unity3d.framework"];/g' ./UnityProject/iosBuild/MainApp/main.mm; \
fi
# ref: https://qiita.com/tkyaji/items/7dbd56b41b6ac3e72635#2-3-displaymanagermm-を修正
.PHONY: fix-display-manager
fix-display-manager:
sed -i '' -e '/surface->memorylessDepth = params\.metalMemorylessDepth;/{' \
-e 'N;N;N' \
-e 's/surface->memorylessDepth = params\.metalMemorylessDepth;\n\n const int api = UnitySelectedRenderingAPI();\n if (api == apiMetal)$$/surface->memorylessDepth = params.metalMemorylessDepth;\n\n const int api = UnitySelectedRenderingAPI();\n if (api == apiMetal \&\& params.renderW != 0 \&\& params.renderH != 0)/g' \
-e '}' \
./UnityProject/iosBuild/Classes/Unity/DisplayManager.mm
私は試したことがないですが、 [PostProcessBuild] を使うほうが正攻法です。気になる人は以下の記事を参考に試してみてください。
Frameworkの生成
xcodebuild archive を実行して .framework ファイルを実機用とシミュレータ用で2つ生成します。
PROJECT_ROOT := ./UnityProject
IOS_DEVICE_ROOT := $(PROJECT_ROOT)/iosBuild
IOS_DEVICE_PROJECT_PATH := $(IOS_DEVICE_ROOT)/Unity-iPhone.xcodeproj
IOS_DEVICE_ARCHIVE_PATH := $(IOS_DEVICE_ROOT)/UnityFramework-Device.xcarchive
IOS_SIMULATOR_ROOT := $(PROJECT_ROOT)/iosEmptyBuild
IOS_SIMULATOR_PROJECT_PATH := $(IOS_SIMULATOR_ROOT)/UnityFramework.xcodeproj
IOS_SIMULATOR_ARCHIVE_PATH := $(IOS_SIMULATOR_ROOT)/UnityFramework-Simulator.xcarchive
.PHONY: archive-ios-device
archive-ios-device:
xcodebuild \
-sdk iphoneos \
-project $(IOS_DEVICE_PROJECT_PATH) \
-scheme 'UnityFramework' \
-destination='iOS' \
-archivePath $(IOS_DEVICE_ARCHIVE_PATH) \
SKIP_INSTALL=NO \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
OTHER_SWIFT_FLAGS="-Xfrontend -empty-abi-descriptor" \
clean archive
.PHONY: archive-ios-simulator
archive-ios-simulator:
xcodebuild \
-sdk iphonesimulator \
-project $(IOS_SIMULATOR_PROJECT_PATH) \
-scheme 'UnityFramework' \
-destination='iOS Simulator' \
-archivePath $(IOS_SIMULATOR_ARCHIVE_PATH) \
SKIP_INSTALL=NO \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
OTHER_SWIFT_FLAGS="-Xfrontend -empty-abi-descriptor" \
clean archive
シミュレータ用が空のプロジェクトであること以外、基本的には参考にした記事の通りです。
ひとつ大きく異なるのが、 OTHER_SWIFT_FLAGS="-Xfrontend -empty-abi-descriptor" の追加です。
これがないと UnityFramework.xcframework/ios-arm64/UnityFramework.framework/Modules/UnityFramework.swiftmodule/arm64-apple-ios.abi.json にソースコードのフルパスが記載されてしまいます。
以下の記事を参考にして追加しました。
XCFrameworkの生成
先ほど生成した2つの .framework ファイルを xcodebuild -create-xcframework コマンドを使ってがっちゃんこします。
XCFRAMEWORK_PATH := $(PROJECT_ROOT)/UnityFramework.xcframework
.PHONY: create-xcframework
create-xcframework:
xcodebuild \
-create-xcframework \
-framework '$(IOS_DEVICE_ARCHIVE_PATH)/Products/Library/Frameworks/UnityFramework.framework' \
-framework '$(IOS_SIMULATOR_ARCHIVE_PATH)/Products/Library/Frameworks/UnityFramework.framework' \
-output $(XCFRAMEWORK_PATH)
Swiftインターフェースの修正
このままXCFrameworkをiOSアプリのプロジェクトに組み込むと、以下のビルドエラーが実機のみで発生します。
'XXX' is not a member type of class 'UnityFramework.UnityFramework'`
BUILD_LIBRARY_FOR_DISTRIBUTION=YES を指定しているのが原因らしく、Swiftインターフェース内の UnityFramework. をすべて削除することでビルドが通るようになります。
# ref: https://qiita.com/mao_/items/9874c1efa280ed4bb399#-4-build_library_for_distributionを有効にしている都合でこのままだとunityframeworkxcframeworkを組み込んだ際にエラーが発生するので対策
.PHONY: fix-swiftinterfaces
fix-swiftinterfaces:
cd $(XCFRAMEWORK_PATH)
find ./ -name '*.swiftinterface' | xargs sed -i '' 's/UnityFramework\.//g'
find ./ -name '*.swiftinterface-e' | xargs rm -f
sed コマンドで編集すると *.swiftinterface-e のバックアップファイルが作成されるので、それも削除します。
Makefileの作成
あとは成果物を削除する make clean と、すべてを実行する make ios を追加したら Makefile の完成です。
make ios を実行するだけでXCFrameworkが生成されます。
Makefile の全貌を紹介します。
# ref: https://qiita.com/mao_/items/9874c1efa280ed4bb399
# ref: https://qiita.com/mao_/items/4da81d6b57ea1a0fe382
# ref: https://github.com/mao-test-h/UaaL-Examples-iOS-6000/blob/9b2847b27f74e1a0932f7f824b02743652b84210/Makefile
PROJECT_ROOT := ./UnityProject
IOS_DEVICE_ROOT := $(PROJECT_ROOT)/iosBuild
IOS_DEVICE_PROJECT_PATH := $(IOS_DEVICE_ROOT)/Unity-iPhone.xcodeproj
IOS_DEVICE_ARCHIVE_PATH := $(IOS_DEVICE_ROOT)/UnityFramework-Device.xcarchive
IOS_SIMULATOR_ROOT := $(PROJECT_ROOT)/iosEmptyBuild
IOS_SIMULATOR_PROJECT_PATH := $(IOS_SIMULATOR_ROOT)/UnityFramework.xcodeproj
IOS_SIMULATOR_ARCHIVE_PATH := $(IOS_SIMULATOR_ROOT)/UnityFramework-Simulator.xcarchive
XCFRAMEWORK_PATH := $(PROJECT_ROOT)/UnityFramework.xcframework
.PHONY: ios
ios:
$(MAKE) clean
$(MAKE) fix-codes
$(MAKE) archive-ios-device
$(MAKE) archive-ios-simulator
$(MAKE) create-xcframework
$(MAKE) fix-swiftinterfaces
.PHONY: fix-codes
fix-codes:
$(MAKE) fix-pbxproj
$(MAKE) fix-main
$(MAKE) fix-display-manager
# ref: https://github.com/Unity-Technologies/uaal-example/blob/e44eadf2979032274b02dc9357964097871ce9c5/docs/ios.md
.PHONY: fix-pbxproj
fix-pbxproj:
if ! grep -qF 'XXXXXXXXXXXXXXXXXXXXXXXX /* Data in Resources */ = {isa = PBXBuildFile; fileRef = XXXXXXXXXXXXXXXXXXXXXXXX /* Data */; };' './UnityProject/iosBuild/Unity-iPhone.xcodeproj/project.pbxproj'; then \
sed -i '' -e 's/\(XXXXXXXXXXXXXXXXXXXXXXXX \/\* AppDelegateListener\.mm in Sources \*\/ = {isa = PBXBuildFile; fileRef = XXXXXXXXXXXXXXXXXXXXXXXX \/\* AppDelegateListener\.mm \*\/; };\)/\1\
\n\t\tXXXXXXXXXXXXXXXXXXXXXXXX \/\* Data in Resources \*\/ = {isa = PBXBuildFile; fileRef = XXXXXXXXXXXXXXXXXXXXXXXX \/\* Data \*\/; };/g' ./UnityProject/iosBuild/Unity-iPhone.xcodeproj/project.pbxproj; \
fi
if ! grep -q ' XXXXXXXXXXXXXXXXXXXXXXXX /\* Data in Resources \*/,' './UnityProject/iosBuild/Unity-iPhone.xcodeproj/project.pbxproj'; then \
sed -i '' 's/\(\t\t\t\t\)\(XXXXXXXXXXXXXXXXXXXXXXXX \/\* PrivacyInfo\.xcprivacy in Resources \*\/,\)/\1XXXXXXXXXXXXXXXXXXXXXXXX \/\* Data in Resources \*\/,\
\n\1\2/' ./UnityProject/iosBuild/Unity-iPhone.xcodeproj/project.pbxproj; \
fi
sed -i '' 's/\(XXXXXXXXXXXXXXXXXXXXXXXX \/\* NativeCallProxy\.h in Headers \*\/ = {isa = PBXBuildFile; fileRef = XXXXXXXXXXXXXXXXXXXXXXXX \/\* NativeCallProxy\.h \*\/;\) };/\1 settings = {ATTRIBUTES = (Public, ); }; };/' ./UnityProject/iosBuild/Unity-iPhone.xcodeproj/project.pbxproj
# ref: https://github.com/Unity-Technologies/uaal-example/blob/e44eadf2979032274b02dc9357964097871ce9c5/docs/ios.md
.PHONY: fix-main
fix-main:
if ! grep -q '\[ufw setDataBundleId: "com\.unity3d\.framework"\];' './UnityProject/iosBuild/MainApp/main.mm'; then \
sed -i '' -e 's/ id ufw = UnityFrameworkLoad();/ id ufw = UnityFrameworkLoad();\n [ufw setDataBundleId: "com.unity3d.framework"];/g' ./UnityProject/iosBuild/MainApp/main.mm; \
fi
# ref: https://qiita.com/tkyaji/items/7dbd56b41b6ac3e72635#2-3-displaymanagermm-を修正
.PHONY: fix-display-manager
fix-display-manager:
sed -i '' -e '/surface->memorylessDepth = params\.metalMemorylessDepth;/{' \
-e 'N;N;N' \
-e 's/surface->memorylessDepth = params\.metalMemorylessDepth;\n\n const int api = UnitySelectedRenderingAPI();\n if (api == apiMetal)$$/surface->memorylessDepth = params.metalMemorylessDepth;\n\n const int api = UnitySelectedRenderingAPI();\n if (api == apiMetal \&\& params.renderW != 0 \&\& params.renderH != 0)/g' \
-e '}' \
./UnityProject/iosBuild/Classes/Unity/DisplayManager.mm
.PHONY: archive-ios-device
archive-ios-device:
xcodebuild \
-sdk iphoneos \
-project $(IOS_DEVICE_PROJECT_PATH) \
-scheme 'UnityFramework' \
-destination='iOS' \
-archivePath $(IOS_DEVICE_ARCHIVE_PATH) \
SKIP_INSTALL=NO \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
OTHER_SWIFT_FLAGS="-Xfrontend -empty-abi-descriptor" \
clean archive
.PHONY: archive-ios-simulator
archive-ios-simulator:
xcodebuild \
-sdk iphonesimulator \
-project $(IOS_SIMULATOR_PROJECT_PATH) \
-scheme 'UnityFramework' \
-destination='iOS Simulator' \
-archivePath $(IOS_SIMULATOR_ARCHIVE_PATH) \
SKIP_INSTALL=NO \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
OTHER_SWIFT_FLAGS="-Xfrontend -empty-abi-descriptor" \
clean archive
.PHONY: create-xcframework
create-xcframework:
xcodebuild \
-create-xcframework \
-framework '$(IOS_DEVICE_ARCHIVE_PATH)/Products/Library/Frameworks/UnityFramework.framework' \
-framework '$(IOS_SIMULATOR_ARCHIVE_PATH)/Products/Library/Frameworks/UnityFramework.framework' \
-output $(XCFRAMEWORK_PATH)
# ref: https://qiita.com/mao_/items/9874c1efa280ed4bb399#-4-build_library_for_distributionを有効にしている都合でこのままだとunityframeworkxcframeworkを組み込んだ際にエラーが発生するので対策
.PHONY: fix-swiftinterfaces
fix-swiftinterfaces:
cd $(XCFRAMEWORK_PATH)
find ./ -name '*.swiftinterface' | xargs sed -i '' 's/UnityFramework\.//g'
find ./ -name '*.swiftinterface-e' | xargs rm -f
.PHONY: clean
clean:
rm -rf $(IOS_DEVICE_ARCHIVE_PATH)
rm -rf $(IOS_SIMULATOR_ARCHIVE_PATH)
rm -rf $(XCFRAMEWORK_PATH)
おわりに
Unity as a LibraryでXCFrameworkを生成できました。
GitHub Actionsで自動化したり、実際にiOSアプリのプロジェクトにSPM経由で導入したりを紹介できていないので、別記事で紹介する予定です。
以上 どすこい塾 Advent Calendar 2025 の14日目の記事でした。
明日も @uhooi です。