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.
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!)
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
}
}
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.
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.
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
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.
Just grab a UIView
's .window?.windowScene
property and configure the sizeRestrictions on it.