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

Swift 无3D资源ARKit应用Multipeer同步UI不渲染解决方案

问题原因

ARKit 协作模式默认仅同步ARAnchor的基础变换、系统内置属性,不会自动同步自定义业务数据,也不会自动触发对端的UI创建逻辑。当前实现无法同步便签的核心原因有三点:

  1. 同步使用的是无业务标识的通用ARAnchor,对端收到锚点后无法识别这是便签锚点,也拿不到便签文本、尺寸等渲染必需的信息
  2. 对端收到同步锚点后,没有执行创建StickyNoteEntity、插入屏幕UITextView、绑定手势的渲染逻辑,自然无法显示便签
  3. 同一放置位置重复添加了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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.01 01:42:23