Skip to content

Instantly share code, notes, and snippets.

@florentmorin
Created February 14, 2024 11:07
Show Gist options
  • Save florentmorin/a462293c9db7f74092aa8775a056398a to your computer and use it in GitHub Desktop.
Save florentmorin/a462293c9db7f74092aa8775a056398a to your computer and use it in GitHub Desktop.
Track scenes state on SwiftUI with visionOS

Track scenes state on SwiftUI with visionOS

Sometimes, a window is closed and you don't know it's closed.

In example:

  • a detail window will be opened from main window
  • user will close main window
  • and there is no way to go back to main window.

This sample helps me to fix it on my own apps.

If you think it can be improved, don't hesitate to comment. ^^

import SwiftUI
/// Main Window content
private struct MainView: View {
@Environment(\.appState) private var appState
@Environment(\.openWindow) private var openWindow
var body: some View {
NavigationStack {
VStack {
Text("Main View")
.navigationTitle("Main Window")
Button {
if !appState.visibleScenes.contains(.detailWindow) {
openWindow(id: AppState.AppSceneId.detailWindow.rawValue)
}
} label: {
Text("Open detail")
}
}
}
.visibilityTracker(for: .mainWindow)
}
}
/// Detail Window content
private struct DetailView: View {
@Environment(\.appState) private var appState
@Environment(\.openWindow) private var openWindow
var body: some View {
NavigationStack {
Text("Detail View")
.navigationTitle("Detail Window")
}
.visibilityTracker(for: .detailWindow)
.onChange(of: appState.visibleScenes) { _, newValue in
// if everything is closed, re-open main window
if newValue.isEmpty {
openWindow(id: AppState.AppSceneId.mainWindow.rawValue)
}
}
}
}
@main
struct MyApp: App {
@State private var appState = AppState()
var body: some Scene {
// The main view
WindowGroup(id: AppState.AppSceneId.mainWindow.rawValue) {
MainView()
}
.windowResizability(.contentSize)
.environment(\.appState, appState)
// The detail view
WindowGroup(id: AppState.AppSceneId.detailWindow.rawValue) {
DetailView()
}
.windowResizability(.contentSize)
.environment(\.appState, appState)
}
}
import Foundation
import Observation
import SwiftUI
/// A global App State
@Observable final class AppState {
/// Identifier for a scene
enum AppSceneId: String {
case mainWindow
case detailWindow
}
/// Currently displayed scenes (automatically updated)
fileprivate(set) var visibleScenes: Set<AppSceneId> = [.mainWindow]
}
// MARK: - SwiftUI
/// Modifier used to track visibility of current window or immersive space
private struct VisibilityTrackerModifier: ViewModifier {
/// The scene we want to track
let trackedScene: AppState.AppSceneId
@Environment(\.appState) private var appState
@Environment(\.scenePhase) private var scenePhase
func body(content: Content) -> some View {
content
.onAppear {
// Forever called
appState.visibleScenes.insert(trackedScene)
}
.onDisappear {
// Called sometimes
appState.visibleScenes.remove(trackedScene)
}
.onChange(of: scenePhase) { _, newScenePhase in
// Forever called
if newScenePhase == .background || newScenePhase == .inactive {
appState.visibleScenes.remove(trackedScene)
}
}
}
}
// MARK: - SwiftUI helpers
extension View {
func visibilityTracker(for trackedScene: AppState.AppSceneId) -> some View {
modifier(VisibilityTrackerModifier(trackedScene: trackedScene))
}
}
private struct AppStateEnvironmentKey: EnvironmentKey {
static let defaultValue = AppState()
}
extension EnvironmentValues {
var appState: AppState {
get {
self[AppStateEnvironmentKey.self]
}
set {
self[AppStateEnvironmentKey.self] = newValue
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment