Loading
BLOG 開発者ブログ

2025年6月23日

【Apple Vision Pro】端末に保持した3Dモデルを配置する機能を実装する

こんにちは。
アプリ・データソリューショングループのyoshino.kです。

Apple Vision Proの標準機能だけでは、3Dモデルファイルを空間上に3D表示することはできません。
そこで、Apple Vision Pro内に保持した3Dモデルファイルを読み込んで、空間上に配置する機能を実装してみました。
今回はAppleが配布している3Dモデルファイルを使用しています。

目次

  1. 実装した機能のデモ動画
  2. 実装内容の解説
  3. さいごに

実装した機能のデモ動画

アプリを起動すると、「3Dモデルを選択」ボタンを含むビューを表示しています。
ボタンを押すと表示されるファイル選択ビューから、配置したい3Dモデルファイルを選択します。
選択すると、対象の3Dモデルが読み込まれ、空間上に配置されます。

実装内容の解説

空間上にはボタンとRealityViewを配置したシンプルな構成にしています。
3Dモデルファイルの読み込みにはfileImporterを使用し、ボタンタップ時にファイル選択ビューを表示する実装にしています。
(allowsMultipleSelectionをtrueにすると複数ファイルの選択も可能になります。)
選択したファイルの読み込みについては、1点注意があるので触れておきます。

startAccessingSecurityScopedResource()メソッド

fileImporterで取得したファイルはアクセス制限がかかっており、そのままではアクセスできない場合があります。

ファイルの保存場所 アクセス制限
実装したアプリ内 制限なし
実装したアプリ外(他のアプリ内、iCloud Drive内など) 制限あり

デモ動画ではファイルアプリ(他のアプリ)内のファイルを参照しているため、制限がかかっています。
制限がかかっている場合は、startAccessingSecurityScopedResource()メソッドを使うことで、一時的に対象ファイルへのアクセス権を得ることができます。
ファイル使用後は、stopAccessingSecurityScopedResource()メソッドを呼び出し、ファイルシステムリソースへのアクセスを放棄します。
※放棄しないと、セキュリティリスクを孕んだ状態になる上、メモリリークを引き起こす可能性があります。

import SwiftUI
import RealityKit
import UniformTypeIdentifiers

struct ContentView: View {
    @State private var isFileImporterPresented = false
    @State private var modelEntity: ModelEntity? = nil

    var body: some View {
        VStack {
            Button("3Dモデルを選択") {
                isFileImporterPresented = true
            }
            RealityView { content in
                // 初期描画時には何も表示しない
            } update: { content in
                // modelEntity が更新されたら追加
                if let entity = modelEntity {
                    content.entities.removeAll()
                    content.add(entity)
                }
            }
            .frame(width: 600, height: 600)
            .padding()
        }
        .fileImporter(
            isPresented: $isFileImporterPresented,
            allowedContentTypes: [.usdz],
            allowsMultipleSelection: false
        ) { result in
            do {
                let selectedURL: URL = try result.get().first!
                // セキュリティスコープ付きリソースのアクセス
                if selectedURL.startAccessingSecurityScopedResource() {
                    Task {
                        do {
                            // 非同期でModelEntityを読み込み
                            let entity = try await ModelEntity(contentsOf: selectedURL)
                            // 読み込んだModelEntityをStateに代入
                            modelEntity = entity
                        } catch {
                            print("ModelEntity読み込み失敗: \(error)")
                        }
                        selectedURL.stopAccessingSecurityScopedResource()
                    }
                }
            } catch {
                print("ファイル選択失敗: \(error)")
            }
        }
    }
}

さいごに

3Dモデルファイルを読み込んで空間上に配置する機能は、特殊な機能ではないですが使う機会は多いかと思います。
私は実装時に、アクセス制限のところでつまづいたので、アプリ外部のファイルを参照する場合にはご注意ください。

yoshinokのブログ