How to build Synced Window Sets in visionOS
Learn how to use two simple SwiftUI features to build a set of Windows that can be moved as a group.
Overview
This example builds on two simple SwiftUI / visionOS concepts. It’s important that you understand these concepts first. These are defaultWindowPlacement and onGeometryChange3D
Let’s start with a main window and two secondary windows. Notice that the secondary windows use defaultWindowPlacement to open to the left and right of the main window.
struct Garden035App: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.defaultSize(width: 500, height: 500)
WindowGroup(id: "YellowFlower") {
Text("🌸")
.font(.system(size: 128))
}
.defaultSize(CGSize(width: 300, height: 200))
.defaultWindowPlacement { _, context in
if let mainWindow = context.windows.first {
return WindowPlacement(.leading(mainWindow))
}
return WindowPlacement(.none)
}
WindowGroup(id: "PinkFlower") {
Text("🌼")
.font(.system(size: 128))
}
.defaultSize(CGSize(width: 300, height: 200))
.defaultWindowPlacement { _, context in
if let mainWindow = context.windows.first {
return WindowPlacement(.trailing(mainWindow))
}
return WindowPlacement(.none)
}
}
}In our ContentView, we can use onGeometryChange3D with Point3D. This will allow us to read the position of the main window relative to world space. This value changes when the user re-centers their view or when they move the window. With a bit of state tracking, we can use these changes to determine when a window move started or ended. We’ll use a timer and some debouncing logic to create an end/stop condition.
.onGeometryChange3D(for: Point3D.self) { proxy in try! proxy
.coordinateSpace3D()
.convert(value: Point3D.zero, to: .worldReference)
} action: { old, new in
worldPosiiton = new
// Mark as moving when a new change arrives
if !isMoving {
isMoving = true
print("🟢 Window movement began at \(worldPosiiton)")
}
// Reset debounce timer; when it fires, consider movement ended
movementEndTimer?.invalidate()
movementEndTimer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false) { _ in
isMoving = false
// This is your "stopped moving" event
print("🛑 Window movement stopped at \(worldPosiiton)")
}
}
Now we can determine if our secondary windows should open or close. When we start moving the main window, we close the secondary windows. When we stop moving the main window, we reopen them. This concept works well when moving the main window, as the other windows are positioned around that. I’m not sure how this would work when extended to moving the secondary windows.
Video Demo
A video demo shows the secondary windows close and reopen based on the movement of the main window.
Sample code for this post is available in Garden035 in Step Into Examples on GitHub
Download the Xcode project with this and many more examples from Step Into Vision.
Some examples are provided as standalone Xcode projects. You can find those here.


Follow Step Into Vision