Swift 无3D资源ARKit应用Multipeer同步UI不渲染解决方案
ARKit 协作模式默认仅同步ARAnchor的基础变换、系统内置属性,不会自动同步自定义业务数据,也不会自动触发对端的UI创建逻辑。当前实现无法同步便签的核心原因有三点:
- 同步使用的是无业务标识的通用
ARAnchor,对端收到锚点后无法识别这是便签锚点,也拿不到便签文本、尺寸等渲染必需的信息 - 对端收到同步锚点后,没有执行创建
StickyNoteEntity、插入屏幕UITextView、绑定手势的渲染逻辑,自然无法显示便签 - 同一放置位置重复添加了
StickyNoteEntity(本身继承自AnchorEntity可直接被同步)和独立ARAnchor两个锚点,同步时会产生冗余锚点
另外屏幕空间标注的渲染逻辑本身就是各端独立完成3D到2D的坐标投影,不能直接同步本地触摸生成的屏幕坐标——不同设备的相机位置、屏幕尺寸、视场角都存在差异,直接同步屏幕坐标必然出现位置偏移、对端显示错位的问题。
1. 定义可同步的自定义便签锚点
自定义继承自ARAnchor的业务锚点,存储便签唯一ID、文本内容、尺寸等需要同步的信息,注意要实现NSCoding相关序列化方法,协作数据传输时会自动完成归档/反序列化。
// 自定义支持协作同步的便签锚点 class StickyNoteAnchor: ARAnchor { // 所有自定义属性必须支持NSSecureCoding let noteID: String var noteText: String let noteSize: CGSize init(transform: simd_float4x4, noteText: String = "", noteSize: CGSize = CGSize(width: 200, height: 200)) { self.noteID = UUID().uuidString self.noteText = noteText self.noteSize = noteSize super.init(name: "StickyNote", transform: transform) } // 对端接收数据时的反序列化初始化 required init?(coder: NSCoder) { guard let noteID = coder.decodeObject(forKey: "noteID") as? String, let noteText = coder.decodeObject(forKey: "noteText") as? String else { return nil } self.noteID = noteID self.noteText = noteText self.noteSize = coder.decodeCGSize(forKey: "noteSize") super.init(coder: coder) } // 本地发送数据时的序列化 override func encode(with coder: NSCoder) { super.encode(with: coder) coder.encode(noteID, forKey: "noteID") coder.encode(noteText, forKey: "noteText") coder.encode(noteSize, forKey: "noteSize") } // 协作同步必需的安全编码声明 override class var supportsSecureCoding: Bool { return true } }
2. 抽离公共便签创建逻辑
把创建便签实体、插入UI视图、绑定手势的逻辑抽成独立公共方法,本地用户点击创建、对端收到同步锚点时都调用这套逻辑,避免代码重复。
// 修正后的便签插入逻辑 func insertNewSticky(_ sender: UITapGestureRecognizer) { let touchLocation = sender.location(in: arView) guard let raycastResult = arView.raycast(from: touchLocation, allowing: .estimatedPlane, alignment: .any).first else { messageLabel.displayMessage("No surface detected, try getting closer.", duration: 2.0) return } // 创建带业务数据的自定义锚点 let noteAnchor = StickyNoteAnchor(transform: raycastResult.worldTransform) // 本地渲染便签 createLocalStickyNote(from: noteAnchor, at: touchLocation) // 仅需将自定义锚点加入Session,协作模式会自动同步到对端 arView.session.add(anchor: noteAnchor) } // 公共便签渲染方法,本地/对端同步逻辑通用 func createLocalStickyNote(from anchor: StickyNoteAnchor, at screenPosition: CGPoint) { let frame = CGRect(origin: screenPosition, size: anchor.noteSize) let note = StickyNoteEntity(frame: frame, worldTransform: anchor.transform) note.setPositionCenter(screenPosition) // 绑定便签ID,方便后续更新/删除时查找 note.noteID = anchor.noteID arView.scene.addAnchor(note) guard let stickyView = note.view else { return } // 同步锚点携带的文本内容 stickyView.textView.text = anchor.noteText arView.insertSubview(stickyView, belowSubview: trashZone) stickyNoteGestureSetup(note) stickyNotes.append(note) stickyView.textView.delegate = self }
3. 处理对端同步过来的锚点
实现ARSessionDelegate的session(_:didAdd:)方法,过滤出对端同步的便签锚点,将锚点的3D世界坐标投影为当前设备的2D屏幕坐标,调用公共方法渲染便签。
注意:必须过滤当前设备自己创建的锚点,避免重复渲染;不要对同步过来的锚点再次调用
session.add(anchor:),否则会造成循环同步。
func session(_ session: ARSession, didAdd anchors: [ARAnchor]) { for anchor in anchors { // 仅处理自定义便签锚点 guard let noteAnchor = anchor as? StickyNoteAnchor else { continue } // 跳过当前设备自己创建的锚点 if noteAnchor.sessionIdentifier == arView.session.identifier { continue } // 将3D锚点位置投影为当前设备的屏幕坐标 let worldPos = simd_make_float3(noteAnchor.transform.columns.3) let screenPos = arView.project(worldPos) DispatchQueue.main.async { self.createLocalStickyNote(from: noteAnchor, at: screenPos) } } }
4. 补全自定义操作同步逻辑
ARKit协作数据不会自动同步便签的文本编辑、位置拖动、删除等自定义交互,这部分需要通过MultipeerConnectivity自行传输:
- 本地用户编辑文本、移动便签、删除便签时,将对应便签ID、操作类型、变更内容序列化为Data,广播给所有已连接设备
- 对端收到自定义业务数据后,根据便签ID找到本地对应的实体和视图,执行对应的更新/删除操作
5. 补全屏幕坐标实时更新
在RealityKit每帧更新回调中,遍历所有本地便签实体,将实体的3D世界坐标重新投影为2D屏幕坐标,更新对应UITextView的位置,避免设备移动时便签错位。
// 页面加载时注册帧更新回调 arView.scene.subscribe(to: SceneEvents.Update.self) { [weak self] _ in guard let self = self else { return } for note in self.stickyNotes { guard let worldPos = note.position(relativeTo: nil) else { continue } let screenPos = self.arView.project(worldPos) note.view?.center = screenPos } } .store(in: &cancellables)
- 所有位置同步以3D空间的锚点变换为准,禁止同步本地屏幕坐标,所有2D位置必须由各端根据自身相机参数独立投影计算
- 自定义锚点的所有存储属性必须支持
NSSecureCoding,否则协作同步时会出现归档失败、数据丢失问题 - 所有收到同步数据后的UI更新操作必须切换到主线程执行,避免UI不渲染或卡顿
内容的提问来源于stack exchange,提问作者justCubesAndText

