You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI AR场景实现.usdz模型从网络GET请求加载

从网络加载USDZ模型到SwiftUI AR界面的实现方案

问题背景

原本基于SwiftUI开发的AR界面使用本地存储的.usdz模型,需要修改为通过GET请求从网络获取模型。测试模型地址为:https://developer.apple.com/augmented-reality/quick-look/models/vintagerobot2k/robot_walk_idle.usdz,曾尝试参考相关方案但未解决问题,以下是可行实现方法。

现有代码

import SwiftUI
import RealityKit
import ARKit
import FocusEntity

struct ARScreen : View {
    @StateObject var selectedVerb = globalVerb()
    
    @State private var isPlacementEnabled = false
    @State private var selectedModel = "sitdown.usdz"//: String?
    @State private var modelComfiredForPlacement: String?
    
    init() {
        let navBarAppearance = UINavigationController()
        navBarAppearance.navigationBar.largeTitleTextAttributes=[NSAttributedString.Key.foregroundColor:
        UIColor.white]
        UINavigationBar.appearance().tintColor = .black
    }
    
    
    var body: some View {
        NavigationView {
            ZStack(alignment: .bottom) {
                ARViewContainer(modelConfirmedForPlacement: self.$modelComfiredForPlacement)
                
                PlacementButtonView(isPlacementEnabled: self.$isPlacementEnabled, selectedModel: self.selectedModel, modelComfirmedForPlacement: self.$modelComfiredForPlacement)
            }.background(Color("Yellow").ignoresSafeArea())
        }.navigationBarTitle("", displayMode: .inline)
        
        
        
    }
    
    struct ARViewContainer: UIViewRepresentable {
        
        @Binding var modelConfirmedForPlacement: String?
        
        func makeUIView(context: Context) -> ARView {
            
            let arView = CustomARView(frame: .zero)//ARView(frame: .zero)
            
            return arView
            
        }
        
        func updateUIView(_ uiView: ARView, context: Context) {
            if let modelName = self.modelConfirmedForPlacement {
                print("Debug: adding model to scene")
                
                let fileName = modelName + ".usdz"
                
                
                
                let modelEntity = try! ModelEntity.load(named: "\(UserDefaults().value(forKey: "verb")!).usdz")
                
                let anchorEntity = AnchorEntity(plane: .any)
                anchorEntity.addChild(modelEntity)
                
                uiView.scene.addAnchor(anchorEntity)
                
                modelEntity.playAnimation(modelEntity.availableAnimations[0].repeat(),
                                          transitionDuration: 0.5,
                                                startsPaused: false)
                DispatchQueue.main.async {
                    self.modelConfirmedForPlacement = nil
                }
            }
        }
    }
    
    class CustomARView: ARView, FEDelegate {
        let focusSquare = FESquare()
        
        required init(frame frameRect: CGRect) {
            super.init(frame: frameRect)
            
            focusSquare.viewDelegate = self
            focusSquare.delegate = self
            focusSquare.setAutoUpdate(to: true)
        
            self.setupARView()
        }
        func setupARView() {
            let config = ARWorldTrackingConfiguration()
            config.planeDetection = [.horizontal, .vertical]
            config.environmentTexturing = .automatic
            if ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh) {
                config.sceneReconstruction = .mesh
            }
            self.session.run(config)
        }
        @MainActor required dynamic init?(coder decoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    }
}

struct PlacementButtonView: View {
    
    @Binding var isPlacementEnabled: Bool
    var selectedModel: String?
    @Binding var modelComfirmedForPlacement: String?
    
    var body: some View {
        Button(action: {
            print("Debug: Confirm model")
            self.modelComfirmedForPlacement = self.selectedModel
        }) {
            Image(systemName: "checkmark")
                .frame(width: 60, height: 60)
                .font(.title)
                .background(Color.white.opacity(0.75))
                .cornerRadius(30)
                .padding(20)
        }
    }
}

#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        ARScreen()
    }
}
#endif

解决方案

核心思路是先从网络下载USDZ文件到本地临时目录,再从本地路径加载模型到RealityKit中,同时处理异步加载和错误情况。

步骤1:添加网络下载工具函数

新增异步下载函数,处理URL请求与本地存储:

import Foundation

extension URL {
    static func downloadUSDZ(from urlString: String, to tempDir: URL) async throws -> URL {
        guard let url = URL(string: urlString) else {
            throw URLError(.badURL)
        }
        let (data, _) = try await URLSession.shared.data(from: url)
        let fileURL = tempDir.appending(path: "\(UUID().uuidString).usdz")
        try data.write(to: fileURL)
        return fileURL
    }
}

步骤2:修改AR界面的状态与加载逻辑

在ARScreen中添加加载状态与错误提示的状态变量:

@State private var isLoading = false
@State private var loadError: String?

修改ARViewContainer的模型加载逻辑,替换本地加载为异步网络下载+模型加载:

struct ARViewContainer: UIViewRepresentable {
    @Binding var modelConfirmedForPlacement: String?
    @Binding var isLoading: Bool
    @Binding var loadError: String?
    
    func makeUIView(context: Context) -> ARView {
        CustomARView(frame: .zero)
    }
    
    func updateUIView(_ uiView: ARView, context: Context) {
        guard let modelURLString = modelConfirmedForPlacement, !isLoading else { return }
        
        Task {
            DispatchQueue.main.async {
                isLoading = true
                loadError = nil
            }
            
            do {
                let tempDir = FileManager.default.temporaryDirectory
                let localFileURL = try await URL.downloadUSDZ(from: modelURLString, to: tempDir)
                let modelEntity = try ModelEntity.load(contentsOf: localFileURL)
                
                let anchorEntity = AnchorEntity(plane: .any)
                anchorEntity.addChild(modelEntity)
                
                DispatchQueue.main.async {
                    uiView.scene.addAnchor(anchorEntity)
                    
                    if !modelEntity.availableAnimations.isEmpty {
                        modelEntity.playAnimation(modelEntity.availableAnimations[0].repeat(),
                                                  transitionDuration: 0.5,
                                                  startsPaused: false)
                    }
                    modelConfirmedForPlacement = nil
                    isLoading = false
                }
                
                try? FileManager.default.removeItem(at: localFileURL)
            } catch {
                DispatchQueue.main.async {
                    loadError = "模型加载失败:\(error.localizedDescription)"
                    isLoading = false
                    modelConfirmedForPlacement = nil
                }
                print("Error loading model: \(error)")
            }
        }
    }
}

步骤3:更新UI显示加载状态与错误

在ARScreen的视图中添加加载遮罩和错误弹窗:

ZStack(alignment: .bottom) {
    ARViewContainer(modelConfirmedForPlacement: $modelComfiredForPlacement, isLoading: $isLoading, loadError: $loadError)
    
    PlacementButtonView(isPlacementEnabled: $isPlacementEnabled, selectedModel: selectedModel, modelComfirmedForPlacement: $modelComfiredForPlacement)
    
    if isLoading {
        Color.black.opacity(0.5)
            .ignoresSafeArea()
        ProgressView("加载模型中...")
            .foregroundColor(.white)
            .scaleEffect(1.5)
    }
}
.background(Color("Yellow").ignoresSafeArea())
.alert("加载失败", isPresented: .constant(loadError != nil)) {
    Button("确定", role: .cancel) {
        loadError = nil
    }
} message: {
    Text(loadError ?? "")
}

步骤4:调整模型触发逻辑

将selectedModel改为存储网络URL而非本地文件名:

@State private var selectedModel = "https://developer.apple.com/augmented-reality/quick-look/models/vintagerobot2k/robot_walk_idle.usdz"

修改后的完整代码

import SwiftUI
import RealityKit
import ARKit
import FocusEntity
import Foundation

extension URL {
    static func downloadUSDZ(from urlString: String, to tempDir: URL) async throws -> URL {
        guard let url = URL(string: urlString) else {
            throw URLError(.badURL)
        }
        let (data, _) = try await URLSession.shared.data(from: url)
        let fileURL = tempDir.appending(path: "\(UUID().uuidString).usdz")
        try data.write(to: fileURL)
        return fileURL
    }
}

struct ARScreen : View {
    @StateObject var selectedVerb = globalVerb()
    
    @State private var isPlacementEnabled = false
    @State private var selectedModel = "https://developer.apple.com/augmented-reality/quick-look/models/vintagerobot2k/robot_walk_idle.usdz"
    @State private var modelComfiredForPlacement: String?
    @State private var isLoading = false
    @State private var loadError: String?
    
    init() {
        let navBarAppearance = UINavigationController()
        navBarAppearance.navigationBar.largeTitleTextAttributes=[NSAttributedString.Key.foregroundColor:
        UIColor.white]
        UINavigationBar.appearance().tintColor = .black
    }
    
    var body: some View {
        NavigationView {
            ZStack(alignment: .bottom) {
                ARViewContainer(modelConfirmedForPlacement: $modelComfiredForPlacement, isLoading: $isLoading, loadError: $loadError)
                
                PlacementButtonView(isPlacementEnabled: $isPlacementEnabled, selectedModel: selectedModel, modelComfirmedForPlacement: $modelComfiredForPlacement)
                
                if isLoading {
                    Color.black.opacity(0.5)
                        .ignoresSafeArea()
                    ProgressView("加载模型中...")
                        .foregroundColor(.white)
                        .scaleEffect(1.5)
                }
            }
            .background(Color("Yellow").ignoresSafeArea())
            .alert("加载失败", isPresented: .constant(loadError != nil)) {
                Button("确定", role: .cancel) {
                    loadError = nil
                }
            } message: {
                Text(loadError ?? "")
            }
        }
        .navigationBarTitle("", displayMode: .inline)
    }
    
    struct ARViewContainer: UIViewRepresentable {
        @Binding var modelConfirmedForPlacement: String?
        @Binding var isLoading: Bool
        @Binding var loadError: String?
        
        func makeUIView(context: Context) -> ARView {
            CustomARView(frame: .zero)
        }
        
        func updateUIView(_ uiView: ARView, context: Context) {
            guard let modelURLString = modelConfirmedForPlacement, !isLoading else { return }
            
            Task {
                DispatchQueue.main.async {
                    isLoading = true
                    loadError = nil
                }
                
                do {
                    let tempDir = FileManager.default.temporaryDirectory
                    let localFileURL = try await URL.downloadUSDZ(from: modelURLString, to: tempDir)
                    let modelEntity = try ModelEntity.load(contentsOf: localFileURL)
                    
                    let anchorEntity = AnchorEntity(plane: .any)
                    anchorEntity.addChild(modelEntity)
                    
                    DispatchQueue.main.async {
                        uiView.scene.addAnchor(anchorEntity)
                        
                        if !modelEntity.availableAnimations.isEmpty {
                            modelEntity.playAnimation(modelEntity.availableAnimations[0].repeat(),
                                                      transitionDuration: 0.5,
                                                      startsPaused: false)
                        }
                        modelConfirmedForPlacement = nil
                        isLoading = false
                    }
                    
                    try? FileManager.default.removeItem(at: localFileURL)
                } catch {
                    DispatchQueue.main.async {
                        loadError = "模型加载失败:\(error.localizedDescription)"
                        isLoading = false
                        modelConfirmedForPlacement = nil
                    }
                    print("Error loading model: \(error)")
                }
            }
        }
    }
    
    class CustomARView: ARView, FEDelegate {
        let focusSquare = FESquare()
        
        required init(frame frameRect: CGRect) {
            super.init(frame: frameRect)
            
            focusSquare.viewDelegate = self
            focusSquare.delegate = self
            focusSquare.setAutoUpdate(to: true)
        
            setupARView()
        }
        
        func setupARView() {
            let config = ARWorldTrackingConfiguration()
            config.planeDetection = [.horizontal, .vertical]
            config.environmentTexturing = .automatic
            if ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh) {
                config.sceneReconstruction = .mesh
            }
            session.run(config)
        }
        
        @MainActor required dynamic init?(coder decoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    }
}

struct PlacementButtonView: View {
    @Binding var isPlacementEnabled: Bool
    var selectedModel: String?
    @Binding var modelComfirmedForPlacement: String?
    
    var body: some View {
        Button(action: {
            print("Debug: Confirm model")
            modelComfirmedForPlacement = selectedModel
        }) {
            Image(systemName: "checkmark")
                .frame(width: 60, height: 60)
                .font(.title)
                .background(Color.white.opacity(0.75))
                .cornerRadius(30)
                .padding(20)
        }
    }
}

// 保留原自定义类结构
class globalVerb: ObservableObject {}

#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        ARScreen()
    }
}
#endif

关键说明

  • 使用URLSession异步下载USDZ文件到系统临时目录,避免占用永久存储
  • 下载完成后通过ModelEntity.load(contentsOf:)从本地路径加载模型,适配RealityKit的加载逻辑
  • 添加加载状态提示和错误处理,优化用户体验
  • 加载完成后可选择删除临时文件,减少存储占用

内容的提问来源于stack exchange,提问作者Glebzzzik

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 01:42:04