Skip to content

Instantly share code, notes, and snippets.

@drewolbrich
Last active August 12, 2024 02:45
Show Gist options
  • Save drewolbrich/c896a424f08789b42f441e6d8de0a7f8 to your computer and use it in GitHub Desktop.
Save drewolbrich/c896a424f08789b42f441e6d8de0a7f8 to your computer and use it in GitHub Desktop.
A workaround for pushWindow not being available on visionOS 1.
//
// PushWindow_visionOS2.swift
//
// Created by Drew Olbrich on 7/1/24.
// Copyright © 2024 Lunar Skydiving LLC. All rights reserved.
//
// MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import SwiftUI
/// This file defines a view modifier and an environment action that can be used to
/// work around the issue that the `pushWindow` environment action is only defined
/// in visionOS 2.0 and later.
///
/// The specific problem addressed is that the following code will not compile if
/// the deployment version selected in Xcode is visionOS 1.0:
/// ```
/// @Environment(\.pushWindow) var pushWindow
/// ```
/// Because this variable is defined using an environment macro, there's no
/// straightforward way to use `#available` or `@available` to implement a
/// workaround that will compile on both visionOS 1 and visionOS 2, as there would
/// be if `pushWindow` was a method.
///
/// # Example usage:
///
/// In your `App` struct's `body`, define a primary window from which a secondary
/// window will be later pushed, assigning it the `windowPusher_visionOS2` view
/// modifier:
/// ```
/// WindowGroup {
/// MyPrimaryWindowView()
/// .windowPusher_visionOS2()
/// }
/// ```
///
/// Then define the secondary window that will be pushed:
/// ```
/// WindowGroup(id: "my-secondary-window") {
/// MySecondaryWindowView()
/// }
/// ```
///
/// Then define `MyPrimaryWindowView`:
/// ```
/// struct MyPrimaryWindowView: View {
///
/// @Environment(\.pushWindow_visionOS2) var pushWindow_visionOS2
///
/// var body: some View {
/// Button("Push Secondary Window") {
/// if #available(visionOS 2.0, *) {
/// // Calls `pushWindow(id:)` from the `windowPusher_visionOS2` view modifier,
/// // assigned to `MyPrimaryWindowView` above.
/// // This pushes `MySecondaryWindowView`.
/// pushWindow_visionOS2(id: "my-secondary-window")
/// } else {
/// // TODO: Present `MySecondaryWindowView` using a mechanism supported by visionOS 1,
/// // for example using the `sheet` view modifier.
/// }
/// }
/// }
///
/// }
/// ```
extension View {
/// Adds an action that responds to a `pushWindow_visionOS2` environment action by
/// calling `pushWindow` on visionOS 2, and is a no-op on visionOS 1.
func windowPusher_visionOS2() -> some View {
if #available(visionOS 2.0, *) {
return modifier(PushWindowViewModifier_visionOS2())
} else {
return self
}
}
}
@available(visionOS 2.0, *)
private struct PushWindowViewModifier_visionOS2: ViewModifier {
@Environment(\.pushWindow) var pushWindow
func body(content: Content) -> some View {
content
.onPushWindow_visionOS2 { id in
pushWindow(id: id)
}
}
}
@MainActor struct PushWindowAction_visionOS2 {
typealias Action = (_ id: String) -> Void
let action: Action
@MainActor func callAsFunction(id: String) {
action(id)
}
// TODO: Define `callAsFunction(id:value:)` and `callAsFunction(value:)` for full compatibility with `pushWindow`.
}
private struct PushWindowActionKey_visionOS2: EnvironmentKey {
static var defaultValue: PushWindowAction_visionOS2?
}
extension EnvironmentValues {
/// A window push action stored in a view's environment.
///
/// Use the `pushWindow_visionOS2` environment value to get an
/// `PushWindowAction_visionOS2` instance for a given `Environment`. Then call the
/// instance to push a window on visionOS 2.
///
/// This action has no effect on visionOS 1.
///
/// The window from which the window is pushed must be assigned the
/// `onPushWindow_visionOS2` view modifier.
var pushWindow_visionOS2: PushWindowAction_visionOS2 {
get {
self[PushWindowActionKey_visionOS2.self] ?? PushWindowAction_visionOS2(action: { _ in })
}
set {
self[PushWindowActionKey_visionOS2.self] = newValue
}
}
}
private extension View {
@available(visionOS 2.0, *) func onPushWindow_visionOS2(_ action: @escaping PushWindowAction_visionOS2.Action) -> some View {
self.environment(\.pushWindow_visionOS2, PushWindowAction_visionOS2(action: action))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment