Skip to content

Instantly share code, notes, and snippets.

@drewolbrich
Last active March 20, 2024 22:22
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save drewolbrich/ca4802c43e6e226e9cdd95a9e52118b3 to your computer and use it in GitHub Desktop.
Save drewolbrich/ca4802c43e6e226e9cdd95a9e52118b3 to your computer and use it in GitHub Desktop.
An example of how to make visionOS volumes work correctly with Settings > Display > Appearance > Window Zoom
//
// ScaledVolumeContentView.swift
// VolumeScaleExample
//
// Created by Drew Olbrich on 11/6/23.
// Copyright © 2023 Lunar Skydiving LLC. All rights reserved.
//
// MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import SwiftUI
import RealityKit
/// As of visionOS 1.0, if a `RealityView` is presented in a volume and the
/// user changes their display size preference using **Settings > Display >
/// Appearance > Window Zoom**, the volume's content will either appear too small or
/// may be clipped to the edges of the volume for small window zoom sizes.
///
/// Apple's volumetric window group example code from
/// https://developer.apple.com/documentation/swiftui/windowstyle/volumetric/
/// is insufficient, and may unexpectedly result in clipped geometry when the user
/// changes their display size preference:
///
/// WindowGroup(id: Module.globe.name) {
/// Globe()
/// .environment(model)
/// }
/// .windowStyle(.volumetric)
/// .defaultSize(width: 0.6, height: 0.6, depth: 0.6, in: .meters)
///
/// To address this issue, `ScaledVolumeContentView`, defined below, uses
/// `GeometryReader3D` to present a `RealityView` with content that is automatically
/// scaled to reflect the size of its enclosing volume.
///
/// The view's `RealityView` has a single entity named `scaledContentEntity` added
/// to it. For the scaling to work correctly, all other entities that appear in the
/// volume must be parented to `scaledContentEntity`.
///
/// `scaledContentEntity` is scaled automatically to account for the display size
/// preference chosen by the user in the visionOS Settings app using
/// **Settings > Display > Appearance > Window Zoom**.
///
/// The simple scaling used here may not be appropriate for every use case. For
/// example, you may hypothetically want to replace the
/// `scaleEntity(_:with:for:defaultVolumeSize:)` method below with more complex code
/// that repositions some entities and scales others.
///
/// However, if you are simply using `RealityView` with a volume, the code below
/// most likely models the behavior you may expect `RealityView` to have, and not
/// the behavior it actually does have.
///
/// Example usage:
///
/// let defaultVolumeSize = Size3D(width: 0.4, height: 0.4, depth: 0.4)
/// ...
///
/// WindowGroup {
/// ScaledVolumeContentView(defaultVolumeSize: defaultVolumeSize)
/// }
/// .windowStyle(.volumetric)
/// .defaultSize(defaultVolumeSize, in: .meters)
///
/// Modify `createVolumeContentEntity` to create your own content for the volume.
///
/// Add attachments and placeholders to `RealityView` in this code as necessary.
struct ScaledVolumeContentView: View {
/// The default size of the volume, in meters, matching the value passed to
/// `defaultSize` when the volume was created.
///
/// This is not the actual size of the volume if the user has selected a display
/// size other than Large in **Settings > Display > Appearance > Window Zoom**.
let defaultVolumeSize: Size3D
/// The entity that the volume's content is parented to.
///
/// As the volume changes size, this entity is scaled to account for the size of the
/// volume, ensuring that the volume's content is not clipped to the edges of the
/// volume.
@State private var scaledContentEntity = Entity()
var body: some View {
GeometryReader3D { proxy in
RealityView { content in
// For this approach to work correctly, all entities in the volume must be parented
// to this entity instead of being added to the `RealityView` directly using
// `RealityViewContent/add`.
content.add(scaledContentEntity)
// Create the volume's content.
let contentEntity = createVolumeContentEntity(forVolumeSize: defaultVolumeSize)
scaledContentEntity.addChild(contentEntity)
// Scale the RealityView's content to compensate for the initial size of the volume.
scaleEntity(scaledContentEntity, with: content, for: proxy, defaultVolumeSize: defaultVolumeSize)
} update: { content in
// This closure is called whenever the size of the volume changes, which will
// happen if the user selects a new value for **Settings > Display > Appearance**.
// Set the scale of the RealityView's content to reflect the updated volume size.
scaleEntity(scaledContentEntity, with: content, for: proxy, defaultVolumeSize: defaultVolumeSize)
}
}
}
/// Returns an entity representing the volume's content.
///
/// In this example, we return a yellow sphere, scaled to fit the size of the volume.
private func createVolumeContentEntity(forVolumeSize volumeSize: Size3D) -> Entity {
let sphereRadius = Float(min(volumeSize.width, volumeSize.height, volumeSize.depth))/2
return Entity.generateYellowSphere(radius: sphereRadius)
}
/// Sets the scale of `entity` to reflect the user's preferred display size,
/// selected with **Settings > Display > Appearance**.
///
/// `defaultVolumeSize` must be set to the default size assigned when the enclosing
/// volume was created.
private func scaleEntity(_ entity: Entity, with realityViewContent: RealityViewContent, for geometryProxy3D: GeometryProxy3D, defaultVolumeSize: Size3D) {
/// The bounding box of the volume's contents, in meters.
///
/// This value will only equal `defaultVolumeSize` if the preferred display size
/// selected by the user with **Settings > Display > Appearance** happens to be
/// Large, which is the default value for visionOS 1.0.
let scaledVolumeContentBoundingBox = realityViewContent.convert(geometryProxy3D.frame(in: .local), from: .local, to: .scene)
let scale = scaledVolumeContentBoundingBox.extents.x/Float(defaultVolumeSize.width)
entity.scale = [1, 1, 1]*scale
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment