1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

UnityFrameworkをSwiftで使う方法

Last updated at Posted at 2026-01-04

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

はじめに

本記事は どすこい塾 Advent Calendar 2025 の17日目の記事です。
昨日も @uhooiUnityFrameworkが提供している各クラスの主要メソッド一覧(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経由で導入する方法 に従って導入します。

Package.swift
// 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 の全貌を見せ、そのあと各処理について説明します。

loki-ios/Packages/LokiPackage/Sources/Core/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 クラスを実装します。

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
// ...
#endif

UnityFrameworkのロード

Unity クラスをシングルトンで用意し、初期化時に UnityFramework をロードして保持します。

参考記事や公式ドキュメントの UnityFrameworkLoad() を参考に loadUnityFramework() を実装しました。

Unity.swift
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() の呼び出しに工夫が必要だったので説明します。

まず、参考記事の通りに実装すると以下のビルドエラーが発生します。

Unity.swift
        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 へ変えることでデバッグビルド時のエラーを解消できます。

Unity.swift
        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 を使うことでデバッグビルドとリリースビルドの両方に成功しました。

Unity.swift
        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ウィンドウを使っているのかもしれません。

Unity.swift
@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 を渡しています。
必要な場合は AppDelegateapplication(_ 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() を呼び出すだけです。

Unity.swift
@MainActor
public final class Unity: NSObject {
    private let unityFramework: UnityFramework

    public func unload() {
        unityFramework.unloadApplication()
    }
}

完全に終了したい場合は unityFramework.quitApplication(_:) を呼び出してください。

Unityビューの取得

Unityビューを取得できるようにします。

実行されていない場合は unityFramework.appController()nil になるので注意です。

Unity.swift
@MainActor
public final class Unity: NSObject {
    private let unityFramework: UnityFramework

    var view: UIView? {
        unityFramework.appController()?.rootView
    }
}
#endif

リスナーの登録

Unityの終了時に処理を実行したい場合、 UnityFrameworkListener に準拠し、それを unityFramework.register(_:) に渡してリスナーを登録します。

Unity.swift
@MainActor
public final class Unity: NSObject {
    private let unityFramework: UnityFramework

    public func run() {
+         unityFramework.register(self)
        unityFramework.setDataBundleId("com.unity3d.framework")

        // ...
    }
}

UnityFrameworkListener プロトコルに従ってデリゲートメソッドを実装します。

Unity.swift
// 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ビューをラップしたビューを用意します。

loki-ios/Packages/LokiPackage/Sources/Core/Unity/UnityView.swift
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ビューを表示する場合は実行しっぱなしでもいいかもしれませんが、基本的には閉じたら終了したほうがいいと思います。

loki-ios/Packages/LokiPackage/Sources/Features/Foo/FooView.swift
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日目の記事でした。
明日は未定です。

参考リンク

1
2
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
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?