Skip to content

Instantly share code, notes, and snippets.

@Matt54
Last active May 10, 2025 18:12
Show Gist options
  • Save Matt54/d8b0e52cb1333bc41e8715f2a1836bad to your computer and use it in GitHub Desktop.
Save Matt54/d8b0e52cb1333bc41e8715f2a1836bad to your computer and use it in GitHub Desktop.
visionOS combine and split window management idea
import SwiftUI
@main
struct CombineAndSplitViewExampleApp: App {
@State var viewStateManager = ViewStateManager()
var body: some Scene {
// Main Window allows us to launch first CombineAndSplitView Window with appropriate ViewWindowID value
// (if we just had CombineAndSplitView open initially, it would be associated with nil)
// There's probably a way to handle the nil case too, but this already covers my needs
WindowGroup(id: mainWindowID) {
MainView()
}
.windowResizability(.contentSize)
WindowGroup(for: ViewWindowID.self) { id in
CombineAndSplitView(windowID: id.wrappedValue?.windowID ?? firstWindowID)
.environment(viewStateManager)
}
.defaultSize(CGSize(width: 350, height: 350)) // sets how far away the window gets placed from the default window placement
.windowResizability(.contentSize)
.defaultWindowPlacement { content, context in
if let mainWindow = context.windows.first {
if viewStateManager.isMainWindowOpen {
// Appear centered with main view
// I know it's depreciated but PushWindowAction has back navigation
return WindowPlacement(.replacing(mainWindow))
} else if viewStateManager.isFirstWindowOpen{
return WindowPlacement(.trailing(mainWindow))
} else {
return WindowPlacement(.leading(mainWindow))
}
}
return WindowPlacement(.none)
}
}
}
struct MainView: View {
@Environment(\.openWindow) var openWindow
var body: some View {
Button(
action: {
// Open instead of push because push would reshow main on first window dismissal
openWindow(value: ViewWindowID(windowID: firstWindowID))
},
label: {
Text("Open Split View")
.font(.largeTitle)
.padding()
}
)
.padding()
}
}
enum ViewConfiguration: Codable {
case leftOnly
case rightOnly
case combined
case split
}
struct CombineAndSplitView: View {
@Environment(ViewStateManager.self) var manager
@Environment(\.openWindow) var openWindow
@Environment(\.dismissWindow) private var dismissWindow
let windowID: String
// used for immediate feedback on pressing ornament
@State private var isTransitioning: Bool = false
var body: some View {
HStack {
if isShowingLeft {
ExampleView1().frame(width: viewWidth)
}
if isShowingRight {
ExampleView2().frame(width: viewWidth)
}
}
.frame(minWidth: combinedViewWidth, maxWidth: combinedViewWidth,
minHeight: viewWidth, maxHeight: viewWidth)
.animation(.default, value: manager.configuration)
.ornament(attachmentAnchor: .scene(.topTrailing), contentAlignment: .center) {
if isShowingLeft && !isTransitioning {
Button(action: {
if manager.configuration == .split {
print("Got dismissed with \(otherWindowID)")
dismissWindow(value: ViewWindowID(windowID: otherWindowID))
}
switch manager.configuration {
case .combined:
manager.configuration = .leftOnly
case .leftOnly, .rightOnly, .split:
if manager.configuration == .split {
transitionFromSplitToCombined()
} else {
manager.configuration = .combined
}
}
}, label: {
Image(systemName: "rectangle.portrait.and.arrow.right.fill")
.rotationEffect(Angle(degrees: manager.configuration == .combined ? 180 : 0))
})
.labelStyle(.iconOnly)
}
}
.ornament(attachmentAnchor: .scene(.top), contentAlignment: .bottom) {
if manager.configuration == .combined {
Button(action: {
openWindow(value: ViewWindowID(windowID: otherWindowID))
manager.configuration = .split
}, label: {
Image(systemName: "rectangle.split.2x1.fill")
})
.labelStyle(.iconOnly)
}
}
.ornament(attachmentAnchor: .scene(.topLeading), contentAlignment: .center) {
if isShowingRight && !isTransitioning {
Button(action: {
if manager.configuration == .split {
print("Got dismissed with \(otherWindowID)")
dismissWindow(value: ViewWindowID(windowID: otherWindowID))
}
switch manager.configuration {
case .combined:
manager.configuration = .rightOnly
case .leftOnly, .rightOnly, .split:
if manager.configuration == .split {
transitionFromSplitToCombined()
} else {
manager.configuration = .combined
}
}
}, label: {
Image(systemName: "rectangle.portrait.and.arrow.right.fill")
.rotationEffect(Angle(degrees: manager.configuration != .combined ? 180 : 0))
})
.labelStyle(.iconOnly)
}
}
.onAppear {
// Dismissing main to avoid "Open Split View" button being tapped again
if manager.isMainWindowOpen {
dismissWindow(id: mainWindowID)
manager.isMainWindowOpen = false
}
if windowID == firstWindowID {
manager.isFirstWindowOpen = true
} else {
manager.isSecondWindowOpen = true
}
}
.onDisappear {
if windowID == firstWindowID {
manager.isFirstWindowOpen = false
} else {
manager.isSecondWindowOpen = false
}
}
}
func transitionFromSplitToCombined() {
isTransitioning = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: {
isTransitioning = false
manager.configuration = .combined
})
}
struct ExampleView1: View {
var body: some View {
Text("1")
.foregroundStyle(.yellow)
.font(.extraLargeTitle)
}
}
struct ExampleView2: View {
var body: some View {
Text("2")
.foregroundStyle(.green)
.font(.extraLargeTitle)
}
}
var otherWindowID: String {
windowID == firstWindowID ? secondWindowID : firstWindowID
}
let viewWidth: CGFloat = 300
var combinedViewWidth: CGFloat {
manager.configuration == .combined ? 600 : 300
}
var isShowingLeft: Bool {
if manager.configuration == .leftOnly || manager.configuration == .combined {
return true
}
if manager.configuration == .split && windowID == firstWindowID {
return true
}
return false
}
var isShowingRight: Bool {
if manager.configuration == .rightOnly || manager.configuration == .combined {
return true
}
if manager.configuration == .split && windowID == secondWindowID {
return true
}
return false
}
}
@Observable
class ViewStateManager {
var configuration: ViewConfiguration = .leftOnly
var isMainWindowOpen: Bool = true
var isFirstWindowOpen: Bool = false
var isSecondWindowOpen: Bool = false
}
// Some unique type to catch in scene/window launching that passes an associated id
struct ViewWindowID: Hashable, Codable {
let windowID: String
}
// two id's to conveniently juggle between having 1 or 2 windows open
// gives us an id to dismiss the other window as needed
let mainWindowID: String = "MainWindow"
let firstWindowID: String = "1"
let secondWindowID: String = "2"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment