Connecting iPadOS and visionOS apps over the local network
At a glance
| Item | Summary |
|---|---|
| Purpose | Build an iPadOS companion app to control your visionOS app. |
| App architecture | Separate iPadOS and visionOS app targets share a protocol-based peer-messaging package; actor-backed client/server transports own discovery and connections while RobotViewModel maps messages into UI state. |
| Main patterns | SwiftUI scene composition, SwiftUI–RealityKit bridge, Observable state owner, Async update consumer, Protocol-based peer transport, Protocol capability boundary |
| Project style | Code-rich sample with 23 scanned Swift file(s) and 2087 Swift line(s); resources and generated assets are excluded from those counts. |
Project structure
Source bundle/
├── iPadCompanionApp/RobotConfigurationExperience/RobotConfigurationExperienceApp.swift # RobotConfigurationExperienceApp
├── iPadCompanionApp/Companion/CompanionApp.swift # CompanionApp
├── iPadCompanionApp/Packages/PeerToPeerMessaging/Sources/PeerToPeerMessaging/ConnectionTypes.swift # NetworkEvent, ConnectionEvent, NetworkState
├── iPadCompanionApp/Packages/PeerToPeerMessaging/Sources/PeerToPeerMessaging/Actors/Client.swift # Client
├── iPadCompanionApp/Companion/Views/ServerContentView.swift # ServerContentView
├── iPadCompanionApp/SharedContent/Models/RobotViewModel.swift # RobotViewModel
├── iPadCompanionApp/Packages/PeerToPeerMessaging/Sources/PeerToPeerMessaging/Protocols/PeerMessagingProtocols.swift # PeerToPeerMessage, PeerMessagingManager
├── iPadCompanionApp/Packages/PeerToPeerMessaging/Sources/PeerToPeerMessaging/TLS/CertificateTrustManager.swift # CertificateTrustManager
└── iPadCompanionApp/Packages/RealityKitContent/Package.realitycomposerpro/ProjectData/main.json # authored RealityKit content
Structure observations
- The runtime boundary is WindowGroup + volumetric window; the pruned tree lists only files that explain lifecycle, state, or framework integration.
- App code is split into role-named views, models, managers, providers, components, or systems.
- Authored
.realityor Reality Composer Pro content is a real implementation boundary; Swift loads or drives it rather than reproducing its entity graph.
Overall architecture
flowchart LR
CompanionApp_1["CompanionApp"]
WindowGroup___volumetric_window_2["WindowGroup + volumetric window"]
ServerContentView_3["ServerContentView"]
RobotViewModel_4["RobotViewModel"]
RealityView_entity_graph_5["RealityView entity graph"]
Peer_transport___Network_framework_6["Peer transport / Network.framework"]
CompanionApp_1 --> WindowGroup___volumetric_window_2
WindowGroup___volumetric_window_2 --> ServerContentView_3
ServerContentView_3 --> RobotViewModel_4
RobotViewModel_4 --> RealityView_entity_graph_5
RealityView_entity_graph_5 --> Peer_transport___Network_framework_6
Reference code
iPadCompanionApp/Companion/CompanionApp.swift:12 — the app or executable entry declares the outer scene lifecycle.
struct CompanionApp: App {
// ...
}The diagram is a responsibility flow, not a claim that every adjacent node directly calls the next. It keeps scene ownership, shared state, RealityKit content, and framework-provider work at separate levels.
Ownership and state
classDiagram
CompanionApp *-- PeerMessagingController : serverController
CompanionApp *-- RobotViewModel : robotViewModel
ServerContentView o-- ServerController : serverController
ServerContentView o-- RobotViewModel : robotViewModel
Ownership evidence
iPadCompanionApp/Companion/CompanionApp.swift:13 — representative stored state or the nearest verified lifecycle anchor.
@main
struct CompanionApp: App {
@State private var serverController: PeerMessagingController<Server<NetworkCommand>>
// ...
}| Owner | Object or state | Relationship | Mutation authority |
|---|---|---|---|
CompanionApp |
PeerMessagingController as serverController |
owns a SwiftUI-managed reference slot | Only the declaring scope writes |
CompanionApp |
RobotViewModel as robotViewModel |
owns a SwiftUI-managed reference slot | Only the declaring scope writes |
ServerContentView |
ServerController as serverController |
receives a shared/non-owning reference | The upstream owner controls lifetime; this scope may invoke its mutable API |
ServerContentView |
RobotViewModel as robotViewModel |
receives a shared/non-owning reference | The upstream owner controls lifetime; this scope may invoke its mutable API |
Ownership here is deliberately narrow: @Environment and weak references are shared links, initialized @State or stored services are lifecycle ownership, and a RealityView content closure owns additions to its entity graph without making the SwiftUI view a reference-type owner.
Class and protocol design
| Type | Responsibility | Depends on or conforms to |
|---|---|---|
CompanionApp (iPadCompanionApp/Companion/CompanionApp.swift:12) |
Declares app scenes and top-level dependency lifetime. | App |
ServerContentView (iPadCompanionApp/Companion/Views/ServerContentView.swift:11) |
Presents UI and forwards gestures or lifecycle events. | View |
RobotViewModel (iPadCompanionApp/SharedContent/Models/RobotViewModel.swift:16) |
Owns UI-facing feature state and commands. | Concrete framework collaborators |
PeerToPeerMessage (iPadCompanionApp/Packages/PeerToPeerMessaging/Sources/PeerToPeerMessaging/Protocols/PeerMessagingProtocols.swift:13) |
Declares a replaceable capability or lifecycle contract. | Codable, Sendable, Hashable |
PeerMessagingManager (iPadCompanionApp/Packages/PeerToPeerMessaging/Sources/PeerToPeerMessaging/Protocols/PeerMessagingProtocols.swift:17) |
Declares a replaceable capability or lifecycle contract. | Concrete framework collaborators |
RobotConfigurationExperienceApp (iPadCompanionApp/RobotConfigurationExperience/RobotConfigurationExperienceApp.swift:12) |
Declares app scenes and top-level dependency lifetime. | App |
CertificateTrustManager (iPadCompanionApp/Packages/PeerToPeerMessaging/Sources/PeerToPeerMessaging/TLS/CertificateTrustManager.swift:17) |
Coordinates long-lived framework or cross-view work. | Sendable |
Verified protocol-oriented seams: NetworkCommand → PeerToPeerMessage, RobotEvent → PeerToPeerMessage, MockNetworkActor → PeerMessagingManager. Framework conformances remain adapters, not proof of an app-wide protocol architecture.
Access control
| Symbol | Access | Verified effect | Likely rationale |
|---|---|---|---|
public private(set) var connectionState: NetworkState = .cancelled (iPadCompanionApp/Packages/PeerToPeerMessaging/Sources/PeerToPeerMessaging/PeerMessagingController.swift:24) |
private(set) |
The getter keeps its wider visibility, while mutation stays inside the declaring type and its permitted same-file extensions. | Inference: Protect a state invariant while allowing observation. |
@State private var serverController: PeerMessagingController<Server<Net... (iPadCompanionApp/Companion/CompanionApp.swift:13) |
private |
Use is restricted to the declaration and same-file extensions permitted by Swift. | Inference: Hide implementation details and lifecycle-sensitive state. |
public var incomingMessages: AsyncStream<Manager.Message> { (iPadCompanionApp/Packages/PeerToPeerMessaging/Sources/PeerToPeerMessaging/PeerMessagingController.swift:37) |
public |
The declaration is available to importing modules, subject to its containing type’s visibility. | Inference: Export the type across the package-module boundary. |
No reviewed declaration uses fileprivate, open; unmodified Swift declarations are internal.
Reference code
iPadCompanionApp/Packages/PeerToPeerMessaging/Sources/PeerToPeerMessaging/PeerMessagingController.swift:24 — representative visibility boundary.
public private(set) var connectionState: NetworkState = .cancelled
public private(set) var errorMessage: String?
// Tasks for monitoring state.
@ObservationIgnored private var monitorTask: Task<Void, Never>?
@ObservationIgnored private var networkTask: Task<Void, Error>?Logic ownership and placement
| Logic | Owning type or file | Placement rationale |
|---|---|---|
| Scene declaration and dependency lifetime | CompanionApp |
The App/entry boundary determines window, volume, and immersive-space lifetime. |
| Presentation, attachments, and gestures | ServerContentView |
SwiftUI view code forwards user intent and RealityView lifecycle events. |
| Shared feature state and commands | RobotViewModel |
A role-named owner prevents sibling views from duplicating transitions. |
Design patterns
| Pattern | Source evidence | Purpose or tradeoff |
|---|---|---|
| SwiftUI scene composition | iPadCompanionApp/Companion/CompanionApp.swift:12 |
Keeps windows, volumes, and immersive-space lifecycle visible at the app boundary. |
| SwiftUI–RealityKit bridge | iPadCompanionApp/SharedContent/Views/RobotView.swift:19 |
Builds and updates a RealityKit entity graph from SwiftUI lifecycle closures. |
| Observable state owner | iPadCompanionApp/SharedContent/Models/RobotViewModel.swift:16 |
Shares feature state across multiple views without moving framework resources into view values. |
| Async update consumer | iPadCompanionApp/Packages/PeerToPeerMessaging/Sources/PeerToPeerMessaging/PeerMessagingController.swift:55 |
Consumes provider or event streams in cancellable structured tasks. |
| Protocol-based peer transport | iPadCompanionApp/Packages/PeerToPeerMessaging/Sources/PeerToPeerMessaging/PeerMessagingController.swift:17 |
Lets actor-backed client, server, and mock transports share one message contract while the controller publishes UI state. |
| Protocol capability boundary | iPadCompanionApp/Packages/PeerToPeerMessaging/Sources/PeerToPeerMessaging/Protocols/PeerMessagingProtocols.swift:13 |
Defines a local capability used by multiple concrete types; this is the verified protocol-oriented seam. |
Naming conventions
- Role suffixes are evidence, not decoration: App:
CompanionApp,RobotConfigurationExperienceApp; ViewModel:RobotViewModel; Manager:CertificateTrustManager,PeerMessagingManager; Client:Client. - Protocols:
PeerToPeerMessage,PeerMessagingManager. - Commands use verb-led methods:
toggleWindowPresentation,start,createConnection,openStream,handleBrowserStateUpdates,invalidate,send,receiveMessages. - Files generally match their primary type;
Views,Models,Managers,Providers,Components,Systems, andPackagesfolders describe architectural roles where present.
Architecture takeaways
- Treat
CompanionAppas the owner of scene declarations, not as the owner of every RealityKit entity created later. - Keep view-local interaction in SwiftUI, but move provider sessions, playback resources, shared game state, or transport state into a stable owner when their lifetime exceeds one render pass.
Source map
| Source file | Relevant symbols |
|---|---|
iPadCompanionApp/RobotConfigurationExperience/RobotConfigurationExperienceApp.swift:12 |
RobotConfigurationExperienceApp |
iPadCompanionApp/Companion/CompanionApp.swift:12 |
CompanionApp |
iPadCompanionApp/Packages/PeerToPeerMessaging/Sources/PeerToPeerMessaging/ConnectionTypes.swift:19 |
NetworkEvent, ConnectionEvent, NetworkState, NetworkServiceConstants, TLSError |
iPadCompanionApp/Packages/PeerToPeerMessaging/Sources/PeerToPeerMessaging/Actors/Client.swift:14 |
Client |
iPadCompanionApp/Companion/Views/ServerContentView.swift:11 |
ServerContentView |
iPadCompanionApp/SharedContent/Models/RobotViewModel.swift:16 |
RobotViewModel |
iPadCompanionApp/Packages/PeerToPeerMessaging/Sources/PeerToPeerMessaging/Protocols/PeerMessagingProtocols.swift:13 |
PeerToPeerMessage, PeerMessagingManager |
iPadCompanionApp/Packages/PeerToPeerMessaging/Sources/PeerToPeerMessaging/TLS/CertificateTrustManager.swift:17 |
CertificateTrustManager |