You can do this by diving down into UIKit, but it requires some clever SwiftUI work if your app is SwiftUI based. (Most new VisionOS apps will be).
If anyone picks up some mistakes in my SwiftUI explanations, or has some code improvements please call them out in the comments.
The View
struct ContentView: View {
@EnvironmentObject var windowSceneBox: WindowSceneBox
var body: some View {
ZStack {
Text("hello")
}
.glassBackgroundEffect()
.onChange(of: windowSceneBox.windowScene) { old, new in
new?.sizeRestrictions?.maximumSize = CGSize(width: 500, height: 500)
}
}
}
As you can see, when the scene of the view is set, the onChange fires and it immediately sets the size of the windowScene via the sizeRestrictions property. The VisionOS window-launch screen will appear large, and then animate smaller after the view loads.
This image shows the window-resize chrome at the appropriate location for a 500x500 window (because it is!)
What the heck is a WindowSceneBox?
I like using SwiftUI environment variables for things like this, but because the view is added to a scene After it initialized I needed an optional property. I chose to wrap it in a "box" type, though there are other ways to achieve an optional Environment variable if you wanted to explore them.
This is just an observable object with an optional scene property. It's what that .onChange is listening to.
class WindowSceneBox: ObservableObject {
@Published var windowScene: UIWindowScene?
init(windowScene: UIWindowScene?) {
self.windowScene = windowScene
}
}
Where is the environmentObject set?
In the window group:
WindowGroup {
WindowSceneReader { scene in
ContentView()
.environmentObject(WindowSceneBox(windowScene: scene))
}
}
Similar to a GeometryReader, I made a View that will watch for the UIScene that a view is assigned to. I named it WindowSceneReader, and similar to GeometryReader, you provide it with a view to display.
Looks Great. Now what the heck is a WindowSceneReader?
struct WindowSceneReader<Content>: View where Content: View {
@State var windowScene: UIWindowScene?
let contents: (UIWindowScene?) -> Content
var body: some View {
contents(windowScene)
.background {
WindowAccessor(windowScene: $windowScene)
.accessibilityHidden(true)
}
}
}
The sceneReader is initialized with generic Content returning closure with a UIWindowScene parameter. The reader maintains the UIWindowScene as its state, so when it changes, it will re-render its body, and therefore invoke the Content closure again.
What sets the windowScene property? How do we tie into UIKit?
In the WindowSceneReader the Content is wrapped in a .background modifier. Within that background is a WindowAccessor that we pass our windowScene State too as a Binding.
This view is:
struct WindowAccessor: UIViewRepresentable {
@Binding var windowScene: UIWindowScene?
func makeUIView(context: Context) -> UIView {
let view = UIView()
DispatchQueue.main.async() {
self.windowScene = view.window?.windowScene // << right after inserted in window
}
return view
}
func updateUIView(_ view: UIView, context: Context) {
}
}
WindowAccessor is a represented UIView whose responsibility is to grab the windowScene after it's been installed in the hierarchy.
The Chain of Events:
The WindowAccessor is an empty UIView that sets the view's windowScene into the Binding. We make it an invisible background of a view.
This binding is a @State of the WindowSceneReader and detects the scene being set. This causes it's content to be recreated, and provides the window scene when it does so.
The Content of the WindowSceneReader is the rest of app window's view hierarchy
The Content of the WindowSceneReader takes the windowScene provided by the reader and sets it into an EnvironmentObject so it's available throughout the entire window's view hierarchy via @EnvironmentObject
Our window's ContentView see's that the windowScene is available and sets the sizeRestrictions, causing the VisionOS window to animate smaller.
Wow that's a lot. What about UIKit?
Just grab a UIView's .window?.windowScene property and configure the sizeRestrictions on it.
Post
Replies
Boosts
Views
Activity
You can not position windows, but if you’re displaying this 3D content in a “window” and a not a volume, why not try a custom ornament?
The HIG recommends them for spatially relevant supplementary controls and information.
Do you need to add a collision component to the model entity for the gestures to “contact” it? That was required in pre-Vision RealityKit
Can we dive into UIKit to change the window size dynamically?
Also the problem with just displaying 2D content in a volume is that the ”window grabber bar” moves around the volume and the user could end up looking at the side of the plane and not see anything.
I agree and filed a feedback for this. Why in the world you can set a volume to be 300x300x1 and not a window is beyond me.
Also Filed FB12639395
Did you solve this??
An apple individual on the Slack Q&A today said that only green indications for planes are expected.
I just encountered this on MacOS and not iOS. Did you figure this out?
I think actions from within a view itself should cause a "drill down" / push onto the navigation stack, and It seems SwiftUI is designed to encourage this pattern as well.
I have a similar dropdown in my app that shows a settings view, and it does so as a "sheet". This ends up looking fine on both iPhone and iPad, so I've kept it.
If you actually want to open a window, you'll need to explore the openWindow utility that's in the swiftUI environment. Opening windows will only work on iPad and Mac, though.
I’ve noticed the same thing. Have you heard from apple about this?
That may not get you all the way there unless you support all file types. Something like the following will only accept images, for example.
@Parameter(title: "Image", description: "Image to copy", supportedTypeIdentifiers: ["public.image"], inputConnectionBehavior: .connectToPreviousIntentResult) var file: IntentFile
The food truck sample app includes some sample app intents.
For any one seeing this in the future "Com.image" is not the correct UTType for image. It's "public.image".
That did not impact the problem at hand here, though.
@Parameter(title: "Image", description: "Image to copy", supportedTypeIdentifiers: ["public.image"], inputConnectionBehavior: .connectToPreviousIntentResult)