How to access own window within SwiftUI view?
SwiftUI Lift-Cycle (SwiftUI 2+)
Here is a solution (tested with Xcode 13.4), to be brief only for iOS
- We need application delegate to create scene configuration with our scene delegate class
@main
struct PlayOn_iOSApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
// ...
}
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
if connectingSceneSession.role == .windowApplication {
configuration.delegateClass = SceneDelegate.self
}
return configuration
}
}
- Declare our
SceneDelegate
and confirm it to both (!!!+)UIWindowSceneDelegate
andObservableObject
class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
var window: UIWindow? // << contract of `UIWindowSceneDelegate`
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
self.window = windowScene.keyWindow // << store !!!
}
}
- Now we can use our delegate anywhere (!!!) in view hierarchy as
EnvironmentObject
, because (bonus of confirming toObservableObject
) SwiftUI automatically injects it intoContentView
@EnvironmentObject var sceneDelegate: SceneDelegate
var body: some View {
// ...
.onAppear {
if let myWindow = sceneDelegate.window {
print(">> window: \(myWindow.description)")
}
}
}
Complete code in project is here
UIKit Life-Cycle
Here is the result of my experiments that looks appropriate for me, so one might find it helpful as well. Tested with Xcode 11.2 / iOS 13.2 / macOS 15.0
The idea is to use native SwiftUI Environment concept, because once injected environment value becomes available for entire view hierarchy automatically. So
- Define Environment key. Note, it needs to remember to avoid reference cycling on kept window
struct HostingWindowKey: EnvironmentKey {
#if canImport(UIKit)
typealias WrappedValue = UIWindow
#elseif canImport(AppKit)
typealias WrappedValue = NSWindow
#else
#error("Unsupported platform")
#endif
typealias Value = () -> WrappedValue? // needed for weak link
static let defaultValue: Self.Value = { nil }
}
extension EnvironmentValues {
var hostingWindow: HostingWindowKey.Value {
get {
return self[HostingWindowKey.self]
}
set {
self[HostingWindowKey.self] = newValue
}
}
}
- Inject hosting window in root ContentView in place of window creation (either in AppDelegate or in SceneDelegate, just once
// window created here
let contentView = ContentView()
.environment(\.hostingWindow, { [weak window] in
return window })
#if canImport(UIKit)
window.rootViewController = UIHostingController(rootView: contentView)
#elseif canImport(AppKit)
window.contentView = NSHostingView(rootView: contentView)
#else
#error("Unsupported platform")
#endif
- use only where needed, just by declaring environment variable
struct ContentView: View {
@Environment(\.hostingWindow) var hostingWindow
var body: some View {
VStack {
Button("Action") {
// self.hostingWindow()?.close() // macOS
// self.hostingWindow()?.makeFirstResponder(nil) // macOS
// self.hostingWindow()?.resignFirstResponder() // iOS
// self.hostingWindow()?.rootViewController?.present(UIKitController(), animating: true)
}
}
}
}
Add the window as a property in an environment object. This can be an existing object that you use for other app-wide data.
final class AppData: ObservableObject {
let window: UIWindow? // Will be nil in SwiftUI previewers
init(window: UIWindow? = nil) {
self.window = window
}
}
Set the property when you create the environment object. Add the object to the view at the base of your view hierarchy, such as the root view.
let window = UIWindow(windowScene: windowScene) // Or however you initially get the window
let rootView = RootView().environmentObject(AppData(window: window))
Finally, use the window in your view.
struct MyView: View {
@EnvironmentObject private var appData: AppData
// Use appData.window in your view's body.
}