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
相关产品推荐
相关产品推荐

