Unity as a Library×Swiftの記事はシリーズになっています。
記事を順番に読み進めると、Unity as a LibraryをSwiftで使えるようになります。
- Unity as a LibraryでXCFrameworkを生成する方法(Swift)
- Unity as a LibraryのXCFrameworkを生成するGitHub Actions
- UnityFrameworkが提供している各クラスの主要メソッド一覧(Objective-C)
- UnityFrameworkをSwiftで使う方法 ←イマココ
- Unity as a LibraryでUnityからSwiftの処理を呼ぶ方法
はじめに
本記事は どすこい塾 Advent Calendar 2025 の17日目の記事です。
昨日も @uhooi で UnityFrameworkが提供している各クラスの主要メソッド一覧(Objective-C) でした。
昨日の記事でUnityFrameworkが提供しているメソッドを紹介したので、本記事ではそれを実際にSwiftで使う方法を紹介します。
環境
- OS:macOS Sequoia 15.6(24G84)
- Swift:6.2.1
UnityFrameworkの導入
まずは UnityFramework を導入します。
方法は何でもいいですが、本記事ではSPM経由で行います。
Unity as a LibraryでXCFrameworkを生成する方法(Swift) で生成した UnityFramework.xcframework を、 XCFrameworkをSPM経由で導入する方法 に従って導入します。
// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "LokiPackage",
platforms: [
.iOS(.v18),
],
dependencies: [
+ .package(path: "./../unity-framework"),
],
targets: [
+ .target(
+ name: "UnityCore",
+ dependencies: [
+ "UnityFramework",
+ ],
+ path: "Sources/Core/Unity",
+ ),
],
)
UnityFramework を各ターゲットに直接追加してもいいですが、私はラップ用のターゲットを噛ませるのが好みなので UnityCore というターゲットを新たに作成し、そこに追加しています。
UnityFrameworkの呼び出し口の実装
UnityFramework を呼び出すクラスを実装します。
基本的には 参考リンク にある記事の通りですが、使いやすいようにカスタマイズしたり、新しいSwiftの仕様に対応されていない箇所を更新したりしています。
Unity呼び出しクラスの全貌
先にUnity呼び出しクラスである Unity.swift の全貌を見せ、そのあと各処理について説明します。
#if targetEnvironment(simulator)
/// シミュレータ用のダミークラス
@MainActor
public final class Unity {
public static let shared = Unity()
private init() {}
public func run() {
print("`Unity.shared.run()` が実行されました。")
}
public func unload() {
print("`Unity.shared.unload()` が実行されました。")
}
}
#else
import MachO
import UnityFramework
@MainActor
public final class Unity: NSObject {
public static let shared = Unity()
private let unityFramework: UnityFramework
var view: UIView? {
unityFramework.appController()?.rootView
}
override init() {
self.unityFramework = Self.loadUnityFramework()
super.init()
}
public func run() {
unityFramework.register(self)
unityFramework.setDataBundleId("com.unity3d.framework")
// スレッド優先度が逆転するのを避けるため、一時的に下げる
let originalPriority = Thread.current.threadPriority
Thread.current.threadPriority = 0.5
unityFramework.runEmbedded(withArgc: CommandLine.argc, argv: CommandLine.unsafeArgv, appLaunchOpts: nil)
unityFramework.appController()?.window.isHidden = true
// 元のスレッド優先度に戻す
Thread.current.threadPriority = originalPriority
}
public func unload() {
unityFramework.unloadApplication()
}
}
// MARK: - UnityFrameworkListener
extension Unity: @MainActor UnityFrameworkListener {
public func unityDidUnload(_ notification: Notification!) {
// TODO: アンロード後の処理を実装する
print(#function)
}
public func unityDidQuit(_ notification: Notification!) {
// TODO: クィット後の処理を実装する
print(#function)
}
}
// MARK: - Privates
private extension Unity {
static func loadUnityFramework() -> UnityFramework {
let bundlePath = Bundle.main.bundlePath
let frameworkPath = bundlePath + "/Frameworks/UnityFramework.framework"
let bundle = Bundle(path: frameworkPath)!
if !bundle.isLoaded {
bundle.load()
}
let ufwc = bundle.principalClass as! UnityFramework.Type
let ufw = ufwc.getInstance()!
if ufw.appController() == nil {
ufw.setExecuteHeader(#dsohandle.assumingMemoryBound(to: mach_header_64.self))
}
return ufw
}
}
#endif
シミュレータ用にダミークラスを用意
今回は UnityFramework.xcframework にシミュレータ用の実装を含んでいないと仮定し、ダミークラスを用意します。
含んでいる場合は不要です。
#if targetEnvironment(simulator) ... #else で括り、実機用と同じシグニチャの Unity クラスを実装します。
#if targetEnvironment(simulator)
/// シミュレータ用のダミークラス
@MainActor
public final class Unity {
public static let shared = Unity()
private init() {}
public func run() {
print("`Unity.shared.run()` が実行されました。")
}
public func unload() {
print("`Unity.shared.unload()` が実行されました。")
}
}
#else
// ...
#endif
UnityFrameworkのロード
Unity クラスをシングルトンで用意し、初期化時に UnityFramework をロードして保持します。
参考記事や公式ドキュメントの UnityFrameworkLoad() を参考に loadUnityFramework() を実装しました。
import MachO
import UnityFramework
@MainActor
public final class Unity: NSObject {
public static let shared = Unity()
private let unityFramework: UnityFramework
override init() {
self.unityFramework = Self.loadUnityFramework()
super.init()
}
}
// MARK: - Privates
private extension Unity {
static func loadUnityFramework() -> UnityFramework {
let bundlePath = Bundle.main.bundlePath
let frameworkPath = bundlePath + "/Frameworks/UnityFramework.framework"
let bundle = Bundle(path: frameworkPath)!
if !bundle.isLoaded {
bundle.load()
}
let ufwc = bundle.principalClass as! UnityFramework.Type
let ufw = ufwc.getInstance()!
if ufw.appController() == nil {
ufw.setExecuteHeader(#dsohandle.assumingMemoryBound(to: mach_header_64.self))
}
return ufw
}
}
setExecuteHeader()の呼び出し
setExecuteHeader() の呼び出しに工夫が必要だったので説明します。
まず、参考記事の通りに実装すると以下のビルドエラーが発生します。
if ufw.appController() == nil {
// ❌: Undefined symbol: __mh_execute_header
var header = _mh_execute_header
ufw.setExecuteHeader(&header)
}
Undefined symbol: __mh_execute_header
Linker command failed with exit code 1 (use -v to see invocation)
どうやらXcode 16から発生するエラーのようで、 _mh_execute_header を _mh_dylib_header へ変えることでデバッグビルド時のエラーを解消できます。
if ufw.appController() == nil {
// 🔺: デバッグビルドはできるが、リリースビルドに失敗する
- var header = _mh_execute_header
+ var header = _mh_dylib_header
ufw.setExecuteHeader(&header)
}
しかし以下の記事の通り、リリースビルドだとエラーのままです。
Build Configurationを Release にすると以下のビルドエラーが発生します。
Undefined symbol: __mh_dylib_header
Linker command failed with exit code 1 (use -v to see invocation)
上記の記事を参考に、 mach_header_64 を使うことでデバッグビルドとリリースビルドの両方に成功しました。
if ufw.appController() == nil {
// ✅: デバッグビルドとリリースビルドの両方に成功する
- var header = _mh_dylib_header
- ufw.setExecuteHeader(&header)
+ ufw.setExecuteHeader(#dsohandle.assumingMemoryBound(to: mach_header_64.self))
}
Unityの実行
Unityを実行する処理を用意します。
本記事ではUnityウィンドウを使わずにビューとして扱うので、 runUIApplicationMain() でなく runEmbedded() を使っています。
その場合、Unityウィンドウを表示しているとビューが触れなくなるので、 unityFramework.appController()?.window.isHidden = true を実行して隠します。
以下のコードが参考になりました。
かなり躓いたのに、他の記事だと特に触れていなかったのが気になりました。
Unityウィンドウを使っているのかもしれません。
@MainActor
public final class Unity: NSObject {
private let unityFramework: UnityFramework
public func run() {
unityFramework.setDataBundleId("com.unity3d.framework")
// スレッド優先度が逆転するのを避けるため、一時的に下げる
let originalPriority = Thread.current.threadPriority
Thread.current.threadPriority = 0.5
unityFramework.runEmbedded(withArgc: CommandLine.argc, argv: CommandLine.unsafeArgv, appLaunchOpts: nil)
unityFramework.appController()?.window.isHidden = true
// 元のスレッド優先度に戻す
Thread.current.threadPriority = originalPriority
}
}
appLaunchOpts は手抜きして nil を渡しています。
必要な場合は AppDelegate の application(_ application:didFinishLaunchingWithOptions:) から渡してください。
スレッド優先度について、以下の実行時エラーが発生したので スレッド優先度の逆転を一時的に回避する方法(Swift) を参考に回避します。
Thread running at QOS_CLASS_USER_INTERACTIVE waiting on a thread without a QoS class specified. Investigate ways to avoid priority inversions
Unityの終了
Unityを終了する処理を用意します。
unityFramework.unloadApplication() を呼び出すだけです。
@MainActor
public final class Unity: NSObject {
private let unityFramework: UnityFramework
public func unload() {
unityFramework.unloadApplication()
}
}
完全に終了したい場合は unityFramework.quitApplication(_:) を呼び出してください。
Unityビューの取得
Unityビューを取得できるようにします。
実行されていない場合は unityFramework.appController() が nil になるので注意です。
@MainActor
public final class Unity: NSObject {
private let unityFramework: UnityFramework
var view: UIView? {
unityFramework.appController()?.rootView
}
}
#endif
リスナーの登録
Unityの終了時に処理を実行したい場合、 UnityFrameworkListener に準拠し、それを unityFramework.register(_:) に渡してリスナーを登録します。
@MainActor
public final class Unity: NSObject {
private let unityFramework: UnityFramework
public func run() {
+ unityFramework.register(self)
unityFramework.setDataBundleId("com.unity3d.framework")
// ...
}
}
UnityFrameworkListener プロトコルに従ってデリゲートメソッドを実装します。
// MARK: - UnityFrameworkListener
extension Unity: @MainActor UnityFrameworkListener {
public func unityDidUnload(_ notification: Notification!) {
// TODO: アンロード後の処理を実装する
print(#function)
}
public func unityDidQuit(_ notification: Notification!) {
// TODO: クィット後の処理を実装する
print(#function)
}
}
これで呼び出し口の実装は一通り完了です。
Unityを呼び出す処理の実装
実際にUnityを実行し、Unityビューを表示するまで実装します。
SwiftUIのビューでラップ
SwiftUIで表示する場合、Unityビューをラップしたビューを用意します。
import SwiftUI
#if targetEnvironment(simulator)
/// シミュレータ用のダミービュー
public struct UnityView: View {
public var body: some View {
Text("シミュレータでは表示できません。実機で確認してください。")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
}
public init() {}
}
#else
public struct UnityView: UIViewRepresentable {
public init() {}
public func makeUIView(context: Context) -> UIView {
Unity.shared.view ?? UIView()
}
public func updateUIView(_ view: UIView, context: Context) {
}
}
#endif
ラップしたUnityビューの表示
ラップしたUnityビューを表示します。
今回は「Unityビューを表示する」ボタンをタップするとフルスクリーンカバーで表示し、左上の閉じるボタンをタップすると閉じます。
表示する前に Unity.shared.run() 、閉じたあとに Unity.shared.unload() を実行するのを忘れないようにしてください。
頻繁にUnityビューを表示する場合は実行しっぱなしでもいいかもしれませんが、基本的には閉じたら終了したほうがいいと思います。
import UnityCore
struct FooView: View {
@State private var isUnityViewPresented = false
var body: some View {
Button("Unityビューを表示する") {
Unity.shared.run()
isUnityViewPresented = true
}
.fullScreenCover(isPresented: $isUnityViewPresented) {
isUnityViewPresented = false
Unity.shared.unload()
} content: {
UnityView()
.overlay(alignment: .topLeading) {
Button {
isUnityViewPresented = false
Unity.shared.unload()
} label: {
Image(systemName: "xmark.circle")
.frame(width: 44, height: 44)
}
.padding(.top, 44)
}
.ignoresSafeArea()
}
}
}
おわりに
UnityFrameworkをSwiftで使い、UnityビューをSwiftUIで表示できました。
これでUnity as a Library×Swiftを完全に理解しました。
以上 どすこい塾 Advent Calendar 2025 の17日目の記事でした。
明日は未定です。