iOS 接入 ObjectTracking 实战
这篇文章用一个真实 iOS App 改造为例,讲如何把 .referenceobject 接入 ARKit,并兼容 iOS 27 的 object tracking 新能力。

iOS App 接入 ObjectTracking:以 Vision Luxury 的 Tracking Tab 为例
前面讲 ObjectTracking 时,我们更多是在解释概念:reference object、Create ML、detectionObjects、trackingObjects、高帧率追踪和 metric pose。
这篇换一个角度,用一个真实 iOS App 的改造作为例子,看看 ObjectTracking 接入到产品里时,代码和架构会遇到哪些问题。
这个例子来自 Vision Luxury。它在 iOS 端新增了一个独立的 Tracking Tab,用 Roulis1.referenceobject 作为追踪文件,并按照 Apple 最新 reference object 文档的方向实现对象追踪。

iOS 对象追踪代码示例
目标:新增一个独立的 Tracking Tab
这次改造的目标很明确:在 iOS App 里新增一个单独的物体追踪入口。
它不是把 ObjectTracking 混进原有商品 AR 放置流程里,而是在底部 Tab 中新增 Tracking:
TrackingTabView() .tabItem {Label("Tracking", systemImage: "viewfinder")}.tag(3)
这样做的好处是功能边界清晰。用户进入 Tracking Tab,就是进入扫描和追踪某个真实物体的流程;离开这个 Tab,原来的 iOS AR 商品放置逻辑不受影响。
为什么要独立 ARSession
Vision Luxury 原本已经有 iOS AR 业务,用 AR session 做商品放置、平面识别和交互。
ObjectTracking 也需要 AR session。如果它复用原有 session,就会遇到一个典型问题:不同功能需要不同 ARWorldTrackingConfiguration。一旦切换 configuration,就可能影响原有 AR 业务。
所以这次实现没有复用现有 AppModel.arSession,而是让 TrackingTabView 自己持有一个独立的 ARView.session。
这个结构把 UI 状态、AR session 控制、对象追踪回调和可视化拆开了。后面如果要替换 reference object、增加调试面板,或者支持多个对象,也更容易扩展。

iOS ObjectTracking 架构:把 SwiftUI 状态、ARView、ARSessionDelegate 和可视化拆开
状态层:把 UI 和 AR 控制器隔开
IOSObjectTrackingState 是 UI 和 AR 控制器之间的桥。
它记录当前状态、选择的追踪模式、当前 configuration 名称,以及追踪到的 anchor 数量:
@MainActor final class IOSObjectTrackingState: ObservableObject {@Published var phase: IOSObjectTrackingPhase = .idle@Published var selectedMode: IOSObjectTrackingRunMode = .moving@Published var activeConfigurationName = NSLocalizedString("Not running", comment: "AR session is not running")@Published var trackedAnchorCount = 0weak var controller: IOSObjectTrackingControlling?var isRunning: Bool {switch phase {case .running,.detected,.lost: truedefault: false}}func start() {controller?.startTracking(mode: selectedMode)}func stop() {controller?.stopTracking()}}
这里没有让 SwiftUI View 直接操作 ARKit。View 只关心 start()、stop() 和状态展示;真正的 session 切换、reference object 加载、anchor 回调,都交给 IOSObjectTrackingARView.Coordinator。
这种分层对 AR 功能很重要。ARKit 的生命周期和 SwiftUI View 的生命周期并不完全等价,把它们硬绑在一起,后续会很难处理暂停、恢复、页面切换和错误状态。
先显示相机预览,而不是黑屏等待
一个实际体验细节是:用户按下 Start 之前,也应该看到相机画面。
所以 ARView 创建完成后,不是等待用户点击按钮,而是先启动一个普通的 ARWorldTrackingConfiguration,只显示 camera preview 和基础世界追踪:
func makeUIView(context: Context) -> ARView {let arView = ARView(frame: .zero)arView.automaticallyConfigureSession = falsecontext.coordinator.attach(to: arView)state.controller = context.coordinatorcontext.coordinator.startCameraPreview()return arView}
预览 session 的实现很轻:func startCameraPreview() {guard ARWorldTrackingConfiguration.isSupported else {state?.phase = .unsupported(NSLocalizedString("World tracking is not supported on this device.",comment: "Unsupported world tracking message"))return}let configuration = ARWorldTrackingConfiguration()configuration.environmentTexturing = .automaticarView.session.delegate = selfarView.session.run(configuration,options: [.resetTracking, .removeExistingAnchors])removeAllAnchors(from: arView)referenceModel = nilstate?.trackedAnchorCount = 0state?.activeConfigurationName = NSLocalizedString("Camera preview",comment: "Camera preview is running")state?.phase = .idle}
这比按 Start 之前留一个空白界面更合理。用户进入页面后能立刻确认摄像头已经打开,知道自己处在 AR 扫描环境里。
停止追踪时也不是让 ARView 变黑,而是回到 camera preview:
func stopTracking() {startCameraPreview()state?.phase = .stopped}
加载 .referenceobject
追踪文件放在 App bundle 中:
Vision Luxury/Roulis1.referenceobject
运行时通过 ARReferenceObject(archiveURL:) 加载:
private func loadReferenceObject() throws -> ARReferenceObject {guard let url = Bundle.main.url(forResource: referenceObjectName,withExtension: "referenceobject") else {throw ObjectTrackingError.referenceObjectMissing(referenceObjectName)}let object = tryARReferenceObject(archiveURL: url)if object.name == nil {object.name = referenceObjectName}return object}
这一步是 iOS ObjectTracking 的入口。App 不需要自己解析 .referenceobject 内部结构,只需要把它加载成 ARReferenceObject,再交给 ARKit configuration。
用 selector 兼容 iOS 27 API
这里有一个现实问题:Apple 文档里的 iOS 27 API 已经提到 ARReferenceObject.usdzFile,以及更适合移动物体的 tracking object 配置。但本地工程当前 SDK 还不是 iOS 27。
如果直接写 typed API,项目会无法编译。
这次实现采用 runtime selector 做兼容。先判断当前 runtime 是否响应新 selector,支持就使用,不支持就降级。
读取内嵌 USDZ:
private func loadEmbeddedUSDZModel(from referenceObject: ARReferenceObject) async -> Entity? { let selector = NSSelectorFromString("usdzFile") guard referenceObject.responds(to: selector), let usdzURL = referenceObject.value(forKey: "usdzFile") as? URL else { return nil } do { return try await Entity(contentsOf: usdzURL) } catch { print("Failed to load embedded USDZ model: \(error)") return nil } }
配置移动物体追踪:
private func configure(referenceObject: ARReferenceObject,mode: IOSObjectTrackingRunMode,configuration: ARWorldTrackingConfiguration) -> String {switch mode {case .moving:let selector = NSSelectorFromString("setTrackingObjects:")if configuration.responds(to: selector) {_ = configuration.perform(selector, with: NSSet(object: referenceObject))return NSLocalizedString("Moving object tracking",comment: "Moving object tracking configuration")}configuration.detectionObjects = [referenceObject]return NSLocalizedString("Stationary detection fallback",comment: "Fallback when moving tracking is unavailable")case .stationary:configuration.detectionObjects = [referenceObject]return NSLocalizedString("Stationary object detection",comment: "Stationary object detection configuration")}}
这段代码表达了一个很实用的策略:
-
iOS 27 runtime 上,如果系统暴露 trackingObjects和usdzFile,就自动走新能力。
-
iOS 26 或更低 runtime 上,降级到 detectionObjects和包围盒显示。
-
工程仍然可以在当前 SDK 下编译通过。
这种做法适合过渡期。等项目升级到 Xcode 27 和 iOS 27 SDK 后,可以把 selector 替换成 typed API,让代码更清晰。
Start Tracking 的流程
按下 Start 后,流程可以概括为:
-
UI 进入 loading 状态。 -
清理旧 anchor。 -
从 bundle 加载 Roulis1.referenceobject。 -
尝试读取 reference object 内嵌 USDZ。 -
创建 ARWorldTrackingConfiguration。 -
根据 Moving 或 Stationary 模式配置 trackingObjects或detectionObjects。 -
运行 AR session。
核心代码是:
private func startTrackingSession(mode: IOSObjectTrackingRunMode) async {// 1. 初始化状态与清理旧会放state?.phase = .loadingstate?.trackedAnchorCount = 0removeAllAnchors(from: arView)do {// 2. 异步加载参考对象与模型资源let referenceObject = tryloadReferenceObject()referenceModel = await loadEmbeddedUSDZModel(from: referenceObject)// 3. 配置 ARWorldTracking 选项let configuration = ARWorldTrackingConfiguration()configuration.environmentTexturing = .automaticlet configurationName = configure(referenceObject: referenceObject,mode: mode,configuration: configuration)// 4. 绑定代理并重启 AR 引擎arView.session.delegate = selfarView.session.run(configuration,options: [.resetTracking, .removeExistingAnchors])// 5. 更新成功状态state?.activeConfigurationName = configurationNamestate?.phase = .running} catch {// 6. 异常捕获与状态回退state?.phase = .failed(error.localizedDescription)}}
这里最关键的是模式选择。Moving 模式优先尝试新 tracking object 路径,Stationary 模式使用传统 detection object 路径。用户看到的是一个简单的分段控件,但背后对应的是 ARKit session 配置差异。

Start Tracking 流程:从 camera preview 切换到 object tracking session
处理 object anchor 回调
ARKit 检测到对象后,会返回 ARObjectAnchor。
实现里为每个 object anchor 创建一个 AnchorEntity(anchor:),再把可视化实体挂上去:
func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {guard let arView else { return }for case let objectAnchor as ARObjectAnchor in anchors {// 1. 创建 RealityKit 锚点实体并克隆基础模型let anchorEntity = AnchorEntity(anchor: objectAnchor)let model = referenceModel?.clone(recursive: true)// 2. 构建并添加可视化追踪组件anchorEntity.addChild(IOSObjectTrackingVisualization.makeEntity(for: objectAnchor,withModel: model))// 3. 设置初始可见性并缓存实体anchorEntity.isEnabled = isObjectAnchorTracked(objectAnchor)anchorEntities[objectAnchor.identifier] = anchorEntity// 4. 将实体挂载到 RealityKit 场景中arView.scene.addAnchor(anchorEntity)// 5. 更新全局 UI 状态state?.trackedAnchorCount = anchorEntities.countstate?.phase = .detected(objectAnchor.referenceObject.name ?? referenceObjectName)}}
更新时,根据 anchor 是否仍处于 tracked 状态显示或隐藏实体:
func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) {for case let objectAnchor as ARObjectAnchor in anchors {// 1. 检查当前物体的追踪状态let isTracked = isObjectAnchorTracked(objectAnchor)// 2. 动态开关 RealityKit 虚拟模型的可见性anchorEntities[objectAnchor.identifier]?.isEnabled = isTracked// 3. 根据追踪结果,切换 UI 的状态机(Detected / Lost)let name = objectAnchor.referenceObject.name ?? referenceObjectNamestate?.phase = isTracked ? .detected(name) : .lost(name)}}
这里要注意一个细节:anchor 存在不代表当前仍被稳定追踪。UI 需要区分 detected 和 lost,否则用户会误以为对象还在被追踪。
可视化:优先 mesh,失败就包围盒
ObjectTracking 的可视化有两个目的:
第一,让用户知道 App 已经识别到目标物体。
第二,让开发者检查 reference object 的比例、中心点和姿态是否正确。
如果能从 .referenceobject 读取内嵌 USDZ,就把模型作为线框叠加到真实物体上。为了在相机画面里足够明显,示例使用棕铜色线框材质:
private static func applyWireframeMaterial(to entity: Entity) {// 1. 如果当前节点是模型节点,应用线框材质if let modelEntity = entity as? ModelEntity {var material = PhysicallyBasedMaterial()material.triangleFillMode = .linesmaterial.faceCulling = .backmaterial.blending = .transparent(opacity: 0.68)material.baseColor = .init(tint: UIColor(red: 0.62, green: 0.32, blue: 0.14, alpha: 1.0))modelEntity.model?.materials = [material]}// 2. 递归遍历所有子节点,确保嵌套模型也能被应用for child in entity.children {applyWireframeMaterial(to: child)}}
示例还增加了坐标轴和名称标签,用来调试对象姿态和对象名称。这类调试辅助在早期非常重要,因为 ObjectTracking 的问题经常不是代码崩溃,而是“看起来偏了一点”“比例不对”“中心点不在预期位置”。

Object anchor 可视化策略:优先使用内嵌 USDZ,失败时回落到包围盒
这个例子最值得保留的经验
第一,.referenceobject 不一定要手动解包。新系统如果提供 usdzFile,可以优先走系统 API 获取内嵌模型,失败再回落到包围盒。
第二,在 SDK 过渡期,可以用 runtime selector 先接入新系统能力。这样既能在旧 SDK 下编译,又能在新 runtime 上提前启用部分能力。等 SDK 升级后,再替换成 typed API。
第三,ObjectTracking 的用户体验应该从 camera preview 开始。先让用户看到真实环境,再用 Start 切换到追踪,比直接从空白界面进入扫描更清楚。
第四,ObjectTracking 最好有明确的状态表达。Ready、Scanning、Tracking、Lost 这些状态不只是技术状态,也是用户理解当前 AR 行为的关键。
这个例子说明,iOS 接入 ObjectTracking 并不只是把 .referenceobject 丢进 bundle,然后跑一个 AR session。真正要处理的是工程边界、API 兼容、资源打包、追踪状态、可视化降级和用户反馈。把这些做好,ObjectTracking 才能从一个技术 demo 变成 App 里可用的功能。
夜雨聆风