如何在不触发updateUIView的前提下将ARKit光线数据传回SwiftUI
解决ARKit光线估算值传递导致updateUIView频繁调用的问题
问题根源
UIViewRepresentable的任何属性(包括@Binding)发生变化时,SwiftUI都会自动调用updateUIView方法。你的代码中把light作为@Binding传给了ARViewContainer,而ARKit会持续更新这个值,导致每次光线估算值变化都会触发updateUIView——但实际上你根本不需要用light去修改ARView的状态,这完全是不必要的调用。
核心解决方案
把light的单向传递从ARViewContainer的属性中剥离,让Coordinator直接与视图模型交互更新光线值,避免因light变化触发updateUIView。同时优化updateUIView内的逻辑,只在真正需要更新ARView的状态(比如visualizationMode或objectToPlace变化)时执行操作。
修改后的代码示例
1. 视图模型(ViewModel)
class ARViewModel: ObservableObject { @Published var light: Float = 0.0 @Published var objectToPlace: ObjectModel? @Published var visualizationMode: VisualizationMode = .ar // 自定义枚举,默认AR模式 }
2. 重构ARViewContainer
struct ARViewContainer : UIViewRepresentable { @ObservedObject var viewModel: ARViewModel // 仅保留需要用来更新ARView状态的绑定 @Binding var objectToPlace: ObjectModel? @Binding var visualizationMode: VisualizationMode func makeUIView(context: Context) -> ARView { print("makeUIView") let arView = CustomArView(frame: .zero) context.coordinator.view = arView arView.session.delegate = context.coordinator return arView } func updateUIView(_ uiView: ARView, context: Context) { let coordinator = context.coordinator // 仅在visualizationMode真正变化时执行状态切换逻辑 if coordinator.previousVisualizationMode != visualizationMode { coordinator.previousVisualizationMode = visualizationMode if visualizationMode == .model { uiView.session.pause() uiView.session.delegate = nil uiView.scene.anchors.removeAll() uiView.removeFromSuperview() uiView.window?.resignKey() } else { // 如果切回AR模式,可在这里重新启动session(按需添加) let config = ARWorldTrackingConfiguration() config.planeDetection = .horizontal config.environmentTexturing = .automatic config.isLightEstimationEnabled = true if ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh) { config.sceneReconstruction = .mesh } uiView.session.run(config) uiView.session.delegate = coordinator } } // 仅在objectToPlace真正变化时执行模型放置逻辑 if coordinator.previousObjectToPlace != objectToPlace { coordinator.previousObjectToPlace = objectToPlace guard let model = objectToPlace else { return } // 移除旧锚点 if let anchor = uiView.scene.anchors.first(where: { $0.name == "default" }) { anchor.children.removeAll(preservingWorldTransforms: false) } // 添加新模型 if let modelEntity = model.entity { modelEntity.transform.rotation = simd_quatf(real: 1.0, imag: SIMD3<Float>(0.0, 0.0, 0.0)) modelEntity.transform.translation = SIMD3<Float>(0.0, 0.0, 0.0) modelEntity.transform.scale = SIMD3<Float>(0.01, 0.01, 0.01) let anchorEntity = AnchorEntity(plane: .horizontal) anchorEntity.name = "default" anchorEntity.addChild(modelEntity) modelEntity.generateCollisionShapes(recursive: true) uiView.installGestures([.scale, .rotation], for: modelEntity) uiView.scene.addAnchor(anchorEntity) } } } func makeCoordinator() -> Coordinator { Coordinator(viewModel: viewModel) } class Coordinator: NSObject, ARSessionDelegate { var view: ARView? let viewModel: ARViewModel // 记录之前的状态,用于判断是否需要更新 var previousVisualizationMode: VisualizationMode? var previousObjectToPlace: ObjectModel? init(viewModel: ARViewModel) { self.viewModel = viewModel } func session(_ session: ARSession, didUpdate frame: ARFrame) { // 直接更新ViewModel的light,不会触发ARViewContainer的属性变化 viewModel.light = Float(frame.lightEstimate?.ambientIntensity ?? 0.0) guard let view = view else { return } for anchor in view.scene.anchors { if anchor.name == "default" && !anchor.children.isEmpty { viewModel.objectToPlace = nil } } } func sessionShouldAttemptRelocalization(_ session: ARSession) -> Bool { return false } } }
3. SwiftUI视图调用
struct ContentView: View { @StateObject private var vm = ARViewModel() var body: some View { ARViewContainer( viewModel: vm, objectToPlace: $vm.objectToPlace, visualizationMode: $vm.visualizationMode ) } }
额外说明
- 剥离
light的@Binding后,ARKit更新光线值只会触发ViewModel的@Published属性变化,进而更新SwiftUI视图,但不会再触发ARViewContainer的updateUIView。 - 通过在Coordinator中记录之前的状态,即使
updateUIView因其他原因被调用(比如父视图重绘),也不会重复执行不必要的ARView操作,进一步提升性能。
内容的提问来源于stack exchange,提问作者Nicola Rigoni
相关产品推荐
相关产品推荐

