+1 (415) 993-7206

Shared AR in 2026: SharePlay and Shared World Anchors vs. MultipeerConnectivity

"Multi-user AR" has meant three different things over the life of ARKit. In 2018 it meant ARWorldMap sharing and then ARKit 3's collaborative sessions, both carried over MultipeerConnectivity. On Apple Vision Pro it means shared world anchors in an immersive space. And across all Apple devices, the framework for who is in the session is now SharePlay (the GroupActivities framework), with RealityKit's entity synchronization doing the content replication. This tutorial sorts out which is which, when the old approach still applies, and builds a small two-player shared-anchor demo.

The three layers, untangled

It helps to separate three questions that the old "Multipeer" approach answered together:

  1. Discovery and transport. How do devices find each other and exchange bytes? MultipeerConnectivity (peer-to-peer Wi-Fi/Bluetooth) or SharePlay (a FaceTime call or a nearby-devices session, with Apple's relay).
  2. Spatial alignment. How do two devices agree where "here" is? ARKit collaborative sessions (iOS) or shared world anchors / SharedCoordinateSpaceProvider (visionOS 26 and later).
  3. Content replication. How does a cube placed by one user show up on the other? RealityKit's SynchronizationComponent with a MultipeerConnectivityService, or your own message protocol.

Old tutorials bundled all three into a MCSession. The modern stack lets you pick per layer.

When MultipeerConnectivity still makes sense

MultipeerConnectivity is not deprecated, and there are real cases where it remains the right transport:

  • No network. A showroom, factory floor, or trade-show booth where devices cannot reach the internet. MultipeerConnectivity works over local Wi-Fi and Bluetooth with no infrastructure.
  • Kiosk and enterprise devices without Apple IDs or FaceTime, which SharePlay requires for its session setup.
  • iPhone-to-iPhone collaborative world tracking. ARWorldTrackingConfiguration.isCollaborationEnabled produces ARSession.CollaborationData blobs that must reach the other device somehow; Multipeer is still the simplest carrier.

The classic iOS pattern is still valid on iOS 26:

import ARKit
import MultipeerConnectivity

final class CollaborationSession: NSObject, ARSessionDelegate, MCSessionDelegate {
    let arSession: ARSession
    let peerID = MCPeerID(displayName: UIDevice.current.name)
    lazy var mcSession = MCSession(peer: peerID, securityIdentity: nil, encryptionPreference: .required)

    init(arSession: ARSession) {
        self.arSession = arSession
        super.init()
        arSession.delegate = self
        mcSession.delegate = self

        let config = ARWorldTrackingConfiguration()
        config.isCollaborationEnabled = true
        config.planeDetection = [.horizontal]
        arSession.run(config)
    }

    // ARKit produced data the peers need to align their maps.
    func session(_ session: ARSession, didOutputCollaborationData data: ARSession.CollaborationData) {
        guard !mcSession.connectedPeers.isEmpty,
              let bytes = try? NSKeyedArchiver.archivedData(withRootObject: data, requiringSecureCoding: true)
        else { return }
        let mode: MCSessionSendDataMode = data.priority == .critical ? .reliable : .unreliable
        try? mcSession.send(bytes, toPeers: mcSession.connectedPeers, with: mode)
    }

    // Bytes from a peer: hand them back to ARKit.
    func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
        if let collab = try? NSKeyedUnarchiver.unarchivedObject(ofClass: ARSession.CollaborationData.self, from: data) {
            arSession.update(with: collab)
        }
    }

    // Remaining MCSessionDelegate requirements omitted for brevity.
    func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {}
    func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {}
    func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {}
    func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {}
}

Once collaboration data flows, anchors added by one device appear in the other's session(_:didAdd:) with anchor.sessionIdentifier telling you who created them. Pair this with Bonjour advertising (MCNearbyServiceAdvertiser / MCNearbyServiceBrowser) for discovery.

Note what this does not give you: it is iPhone/iPad only. There is no isCollaborationEnabled on visionOS.

The modern path: SharePlay + RealityKit sync

For consumer experiences and anything that spans iPhone and Vision Pro, SharePlay is the session layer. Apple's GroupActivities framework handles invitations, membership, and a reliable message channel, and participants can be on a FaceTime call or physically together (visionOS uses nearby-sharing to start a session with people in the same room).

Define the activity

import GroupActivities

struct PlaceCubesActivity: GroupActivity {
    static let activityIdentifier = "com.example.placecubes"

    var metadata: GroupActivityMetadata {
        var meta = GroupActivityMetadata()
        meta.title = "Place Cubes Together"
        meta.type = .generic
        return meta
    }
}

Join the session and open a messenger

import GroupActivities
import Combine

@MainActor
final class SharedSessionModel: ObservableObject {
    @Published var session: GroupSession<PlaceCubesActivity>?
    private var messenger: GroupSessionMessenger?
    private var tasks = Set<Task<Void, Never>>()
    var onCube: ((CubeMessage) -> Void)?

    struct CubeMessage: Codable, Sendable {
        let id: UUID
        let position: SIMD3<Float>
        let colorIndex: Int
    }

    func listen() {
        tasks.insert(Task {
            for await session in PlaceCubesActivity.sessions() {
                configure(session)
            }
        })
    }

    func startActivity() async {
        switch await PlaceCubesActivity().prepareForActivation() {
        case .activationPreferred:
            _ = try? await PlaceCubesActivity().activate()
        default:
            break
        }
    }

    private func configure(_ session: GroupSession<PlaceCubesActivity>) {
        self.session = session
        let messenger = GroupSessionMessenger(session: session, deliveryMode: .reliable)
        self.messenger = messenger

        tasks.insert(Task {
            for await (message, _) in messenger.messages(of: CubeMessage.self) {
                onCube?(message)
            }
        })
        session.join()
    }

    func send(_ cube: CubeMessage) {
        Task { try? await messenger?.send(cube) }
    }
}

Add the GroupActivities capability to the target. In the simulator, SharePlay sessions can be tested between two simulator instances from Xcode 15 onward.

Align space on visionOS with shared world anchors

Messages tell every participant that a cube was placed at coordinates (x, y, z). For those coordinates to mean the same physical spot, the participants must share a coordinate space. On iOS, that is the collaborative session above. On visionOS 26 and later, ARKit provides SharedCoordinateSpaceProvider for people in the same room: each device exchanges coordinate-space data through your messenger, and once aligned, world anchors created by one participant resolve on the others.

import ARKit

@MainActor
final class SharedSpaceModel {
    let session = ARKitSession()
    let sharedSpace = SharedCoordinateSpaceProvider()
    let worldTracking = WorldTrackingProvider()

    func start(send: @escaping (Data) -> Void,
               incoming: AsyncStream<Data>) async throws {
        try await session.run([sharedSpace, worldTracking])

        // Outbound alignment data for the other participants.
        Task {
            while let data = sharedSpace.nextCoordinateSpaceData {
                send(data.data)
            }
        }

        // Inbound alignment data from peers.
        Task {
            for await bytes in incoming {
                sharedSpace.push(data: .init(data: bytes))
            }
        }
    }

    func anchor(at transform: simd_float4x4) async throws -> WorldAnchor {
        let anchor = WorldAnchor(originFromAnchorTransform: transform)
        try await worldTracking.addAnchor(anchor)
        return anchor
    }
}

Once sharedSpace.eventUpdates reports that participants are connected, a WorldAnchor's transform is valid for every device in the session, and the cube message only has to carry the anchor's position.

The exact property names (nextCoordinateSpaceData, push(data:), eventUpdates) are from the visionOS 26 SDK; check Apple's ARKit documentation for the release you target.

The two-player demo, assembled

With the pieces above, the demo is short. Each participant runs a RealityView; a tap places a cube at the tapped location, sends a CubeMessage, and every participant (including the sender) creates the entity when the message arrives, so local and remote behavior are identical:

struct SharedCubesView: View {
    @StateObject private var shared = SharedSessionModel()
    @State private var root = Entity()
    private let palette: [UIColor] = [.systemRed, .systemBlue, .systemGreen]

    var body: some View {
        RealityView { content in
            content.camera = .spatialTracking   // iOS; on visionOS use an ImmersiveSpace
            content.add(root)
            shared.onCube = { message in
                let cube = ModelEntity(mesh: .generateBox(size: 0.08),
                                       materials: [SimpleMaterial(color: palette[message.colorIndex % palette.count], isMetallic: false)])
                cube.name = message.id.uuidString
                cube.position = message.position
                root.addChild(cube)
            }
            shared.listen()
        }
        .gesture(
            SpatialTapGesture()
                .targetedToAnyEntity()
                .onEnded { value in
                    let position = value.convert(value.location3D, from: .local, to: root)
                    shared.send(.init(id: UUID(), position: position, colorIndex: Int.random(in: 0..<3)))
                }
        )
        .toolbar {
            Button("Share") { Task { await shared.startActivity() } }
        }
    }
}

For a tap to land, give the root something to hit: on iOS a large invisible plane entity with a CollisionComponent anchored to the floor works well.

Which one should you use?

SituationRecommendation
Consumer app, iPhone and/or Vision Pro, users may be remoteSharePlay + GroupSessionMessenger; shared world anchors on visionOS, collaborative session on iOS
Same room, Vision Pro onlySharePlay nearby session + SharedCoordinateSpaceProvider
Offline, enterprise, or kiosk iPhones/iPadsMultipeerConnectivity + collaborative ARKit session
Persistent shared content across daysSave and share ARWorldMap (iOS) or persist WorldAnchor ids (visionOS) and relocalize
Large scale, many users, server-authoritative stateYour own backend for state; SharePlay or Multipeer only for alignment

Gotchas

  • SharePlay requires a FaceTime-capable Apple ID on every device and the GroupActivities entitlement; it is not available to Managed Apple IDs in every configuration. Check with your MDM team early.
  • Collaborative ARKit sessions need visual overlap: both devices must see some of the same environment to align. Coach users to scan the same area.
  • Keep entity state small. Send intents (place, move, delete), not transforms every frame; RealityKit can interpolate locally.
  • Test with three participants, not two. Join ordering and late-join state replay are where shared AR demos fall over.

Building a collaborative AR experience and not sure which layer you need? RealityRogue's consultants can scope it with you. Get in touch.