1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Unity as a LibraryでXCFrameworkを生成する方法(Swift)

Last updated at Posted at 2025-12-27

Unity as a Library×Swiftの記事はシリーズになっています。
記事を順番に読み進めると、Unity as a LibraryをSwiftで使えるようになります。

はじめに

本記事は どすこい塾 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を生成する手順を紹介します。

前提条件

最初に前提条件を記載します。

  • 以下の記事が素晴らしく、基本的にはこちらを見れば問題ない
  • シミュレータ用のXcodeプロジェクトは、空のプロジェクトを使う
    • Xcode上で新規作成し、デプロイメントターゲットのみ調整した
    • UnityProject/iosEmptyBuildUnityFramework.xcodeproj という名前で作成した

.gitignore の作成

Gitで管理しないファイルを .gitignore に記述します。

.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

UnityProject/iosBuild/Unity-iPhone.xcodeproj/project.pbxproj
-               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

UnityProject/iosBuild/Unity-iPhone.xcodeproj/project.pbxproj
                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 */; };
UnityProject/iosBuild/Unity-iPhone.xcodeproj/project.pbxproj
                        files = (
+                               XXXXXXXXXXXXXXXXXXXXXXXX /* Data in Resources */,
                                XXXXXXXXXXXXXXXXXXXXXXXX /* PrivacyInfo.xcprivacy in Resources */,
                        );
UnityProject/iosBuild/MainApp/main.mm
     @autoreleasepool
     {
         id ufw = UnityFrameworkLoad();
+        [ufw setDataBundleId: "com.unity3d.framework"];
         [ufw runUIApplicationMainWithArgc: argc argv: argv];
         return 0;
     }

「2-3. DisplayManager.mm を修正」

UnityProject/iosBuild/Classes/Unity/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 の部分は各自差し替えてください。

Makefile
.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つ生成します。

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

.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 コマンドを使ってがっちゃんこします。

Makefile
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. をすべて削除することでビルドが通るようになります。

Makefile
# 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 の全貌を紹介します。

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 です。

参考リンク

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?