--- name: display-access description: Display capability setup, display-capable device selection, UI DSL, icons, buttons, images, and video playback --- # Display Access (iOS) Use `MWDATDisplay` to render content on Meta Ray-Ban Display glasses. Use this skill with `getting-started` and `permissions-registration` when creating a full app. A Display app still needs `Wearables.configure()` at launch, Info.plist URL scheme configuration, URL callbacks through `Wearables.shared.handleUrl(_:)`, and completed Meta AI registration before it can create a session. ## Add the Display module Add `MWDATDisplay` to the same app target that already uses `MWDATCore`. Import it next to core: ```swift import MWDATCore import MWDATDisplay ``` ## Display-specific app configuration Use the getting-started Info.plist setup, then mirror the DisplayAccess sample for Display sessions: - Keep the `CFBundleURLTypes` URL scheme and route callbacks to `Wearables.shared.handleUrl(_:)`. - Under `MWDAT`, set `AppLinkURLScheme`, `MetaAppID`, `ClientToken`, `TeamID`, and `DAMEnabled = true`. `MetaAppID = 0` is for Developer Mode; production apps should use Wearables Developer Center values for `MetaAppID` and `ClientToken`, plus the Apple Developer Team ID. - Include `UISupportedExternalAccessoryProtocols` with `com.meta.ar.wearable`, `UIBackgroundModes` entries for `external-accessory` and `bluetooth-central`, and `NSBluetoothAlwaysUsageDescription`. The DisplayAccess sample also includes `bluetooth-peripheral` and `processing`. - For high-bandwidth Display/video fallback, include a non-empty `NSLocalNetworkUsageDescription` and `NSBonjourServices`. Display acquires link leases: core device discovery/session setup uses medium then low links, while Display uses medium then high links when available. ## Select a display-capable device Use the public display filter when creating an automatic selector: ```swift let wearables = Wearables.shared let deviceSelector = AutoDeviceSelector( wearables: wearables, filter: { device in device.supportsDisplay() } ) ``` Use `SpecificDeviceSelector(device: selectedDevice.identifier)` instead when your UI lets the user pick a specific `Device`. The initializer takes a `DeviceIdentifier`, not the whole `Device`. `AutoDeviceSelector` updates from `devicesStream()`. Create it before the user taps the Display action, or wait for `activeDeviceStream()` to yield a non-nil device before calling `createSession(deviceSelector:)`; otherwise `createSession` can throw `DeviceSessionError.noEligibleDevice`. For a picker or Settings screen, list devices from `Wearables.shared.devicesStream()`, then look up metadata from `deviceForIdentifier(_:)`. The DisplayAccess sample shows `nameOrId()`, `deviceType().rawValue`, `linkState`, and `compatibility()` and keeps `addLinkStateListener` / `addCompatibilityListener` tokens alive while rows are visible. If `compatibility() == .deviceUpdateRequired`, surface a firmware update action through `Wearables.shared.openFirmwareUpdate()`. ## Attach Display after the session starts Create and start the `DeviceSession`, wait for `.started`, then add and start `Display`. Keep the state listener token alive for as long as you need updates, and observe `session.errorStream()` for async session failures. ```swift import MWDATCore import MWDATDisplay @MainActor final class DisplayController { private var deviceSession: DeviceSession? private var display: Display? private var displayStateToken: AnyListenerToken? private var sessionErrorTask: Task? func connect() async { do { let wearables = Wearables.shared let selector = AutoDeviceSelector( wearables: wearables, filter: { $0.supportsDisplay() } ) let session = try wearables.createSession(deviceSelector: selector) deviceSession = session sessionErrorTask = Task { [weak self] in for await error in session.errorStream() { await self?.showError(error.localizedDescription) } } let sessionStarted = Task { for await state in session.stateStream() { if state == .started { return } } } do { try session.start() await sessionStarted.value } catch { sessionStarted.cancel() throw error } let capability = try session.addDisplay() display = capability displayStateToken = capability.statePublisher.listen { [weak self] state in Task { @MainActor in if state == .started { self?.showStatusCard() } } } await capability.start() } catch DeviceSessionError.datAppOnTheGlassesUpdateRequired { showDATGlassesAppUpdate() } catch { showError(error.localizedDescription) } } func disconnect() async { display?.onPlaybackEvent = nil await display?.stop() deviceSession?.stop() sessionErrorTask?.cancel() sessionErrorTask = nil displayStateToken = nil display = nil deviceSession = nil } } ``` For user-triggered content, match the sample's pending-action pattern: if the user taps "Try it" before Display is connected, store the send as a pending async action, attach to Display, and run the action when `DisplayState.started` arrives. Reset the display session when registration changes to `.available` or `.unavailable`. ## Send display UI Build exactly one root `DisplayableView` per `send(_:)` call. Each send replaces the previous content on the glasses and replaces the active tap handlers. Use a root `FlexBox` for UI, or a root `VideoPlayer` for video. Do not send `Text`, `Button`, `Image`, or `Icon` as the root; place them inside a `FlexBox`. If a file also imports SwiftUI, Display DSL names such as `Text`, `Button`, and `Image` can be ambiguous. Prefer keeping Display builders in files that import `MWDATDisplay` without SwiftUI, or qualify the symbols as `MWDATDisplay.Text`, `MWDATDisplay.Button`, and `MWDATDisplay.Image`. ```swift func showStatusCard() { Task { do { try await display?.send( FlexBox(direction: .column, spacing: 12) { Text("Bike ride", style: .heading) Text("Turn right in 200 ft", style: .body, color: .secondary) Button( label: "Done", style: .primary, iconName: .checkmark, onClick: { print("Done tapped") } ) } .padding(24) .background(.card) .onTap { print("Card tapped") } ) } catch { showError((error as? DisplayError)?.description ?? error.localizedDescription) } } } ``` ## Use images and built-in icons Use HTTPS image URLs for image content, and use the `IconName` enum for built-in icons. Do not invent raw icon strings. ```swift try await display.send( FlexBox(direction: .row, spacing: 8, crossAlignment: .center) { Image( uri: "https://example.com/thumbnail.png", sizePreset: .fill, cornerRadius: .medium ) Icon(name: .gear, style: .filled) Text("Device settings", style: .body) } .padding(24) ) ``` ## Send video For URL-based video, send a root `VideoPlayer`. Use `VideoPlayer(onError:)` for video-specific errors, and use `display.onPlaybackEvent` for playback events. Set `onPlaybackEvent` before sending the video, clear it after terminal events if the flow is complete, and call `sendVideoStop()` if the user exits playback early. Blank or non-HTTP(S) URLs throw `DisplayError.invalidVideoURL`. ```swift display.onPlaybackEvent = { event in if event.type == .ended || event.type == .stopped { Task { @MainActor in display.onPlaybackEvent = nil showStatusCard() } } } try await display.send( VideoPlayer( provider: .uri("https://example.com/tutorial.mp4"), codec: .mp4, onError: { error in Task { @MainActor in showError(error.localizedDescription) } } ) ) ``` ## Display rules - Call `Wearables.configure()` at app launch and complete registration before creating the session. - Enable `MWDAT.DAMEnabled` for Display sessions, and include the DisplayAccess sample's link-lease Info.plist keys when building a full Display app. - Wait for the `DeviceSession` to reach `.started` before calling `addDisplay()`. - Handle `DeviceSessionError.datAppOnTheGlassesUpdateRequired` separately and offer `Wearables.shared.openDATGlassesAppUpdate()`. - Call `await display.start()`, then wait for `DisplayState.started` through `statePublisher` before sending user-triggered content. - Observe `session.errorStream()` so async session failures are surfaced. - Keep listener tokens alive; dropping a token stops that listener. - Use the getting-started setup for Info.plist URL schemes and route app-open URLs to `Wearables.shared.handleUrl(_:)`. - Use `nameOrId()`, `deviceType()`, `linkState`, and `compatibility()` for device rows; keep link/compatibility listener tokens until the row is gone. - Use `FlexBox.onTap` and `Button(label:onClick:)` for interactions. The callbacks belong to the most recent sent view. - Use only public Display DSL names: `FlexBox`, `Text`, `Button`, `Image`, `Icon`, `VideoPlayer`, `IconName`, `TextStyle`, `TextColor`, `ButtonStyle`, `ImageSize`, `CornerRadius`, `Direction`, `Alignment`, `Background`, `Edge`, and `EdgeInsets`. - Clear `display.onPlaybackEvent` when the video flow is finished if you no longer need playback callbacks. - Stop Display before stopping the parent `DeviceSession` when the display experience ends. ## Sample app Use the Display Access sample app for a complete flow: registration, device selection, display attachment, interactive content, and video. ## Links - [iOS API reference](https://wearables.developer.meta.com/docs/reference/ios_swift/dat/0.7) - [Developer documentation](https://wearables.developer.meta.com/docs/develop/)