22

The goal is to have easy access to hosting window at any level of SwiftUI view hierarchy. The purpose might be different - close the window, resign first responder, replace root view or contentViewController. Integration with UIKit/AppKit also sometimes require path via window, so…

What I met here and tried before,

something like this

let keyWindow = shared.connectedScenes
        .filter({$0.activationState == .foregroundActive})
        .map({$0 as? UIWindowScene})
        .compactMap({$0})
        .first?.windows
        .filter({$0.isKeyWindow}).first

or via added in every SwiftUI view UIViewRepresentable/NSViewRepresentable to get the window using view.window looks ugly, heavy, and not usable.

Thus, how would I do that?

Asperi
  • 228,894
  • 20
  • 464
  • 690

7 Answers7

33

SwiftUI Lift-Cycle (SwiftUI 2+)

Here is a solution (tested with Xcode 13.4), to be brief only for iOS

  1. 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
    }
}
  1. Declare our SceneDelegate and confirm it to both (!!!+) UIWindowSceneDelegate and ObservableObject
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 !!!
    }
}

  1. Now we can use our delegate anywhere (!!!) in view hierarchy as EnvironmentObject, because (bonus of confirming to ObservableObject) SwiftUI automatically injects it into ContentView
    @EnvironmentObject var sceneDelegate: SceneDelegate
    
    var body: some View {
         // ...       
            .onAppear {
                if let myWindow = sceneDelegate.window {
                    print(">> window: \(myWindow.description)")
                }
            }
    }

demo3

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

  1. 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
        }
    }
}
  1. 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
  1. 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)
            }
        }
    }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • 6
    Any idea on how to do this if you are using the new `@main` "only SwiftUI" stuff? – Nicolai Henriksen Jul 13 '20 at 09:57
  • 1
    You'd have to use a `UIViewRepresentable` with an (empty) `UIView` that grabs and reports its `window` as it is attached/detached. – hnh Oct 21 '20 at 16:12
  • 2
    @hnh, https://stackoverflow.com/a/63276688/12299030 – Asperi Oct 21 '20 at 16:18
  • @NicolaiHenriksen did you ever find a solution? I'm also stuck on the exact same thing. – aheze Dec 27 '21 at 21:48
  • @aheze, here is possible solution https://stackoverflow.com/a/63276688/12299030. – Asperi Dec 28 '21 at 06:12
  • @Asperi, thanks, I ended up doing that too. But if you put it inside a `LazyVStack` and scroll, `view?.window` becomes `nil` and `makeUIView` gets called multiple times. I'll probably post a question once I do some more debugging. – aheze Dec 28 '21 at 06:58
  • @aheze, we don't need multiple accessors to one window, so that modifier should be placed once into root view and after fetching window just inject it where needed, eg. via environment value or something. – Asperi Dec 28 '21 at 07:38
  • @Asperi (: wow, root view + environment object is exactly what I did. See [here](https://github.com/aheze/Popovers/blob/5fcaa9d9eb2ed077cd43e323b5a772a04bd6e1be/Sources/PopoverFrameTag.swift#L68). The problem is, I had to hardcode another `0.5s` delay, since the view was getting added before the window existed. And keeping a reference to the environment object needed `@StateObject`, but I wanted to have an iOS 13 deployment target. – aheze Dec 28 '21 at 07:47
  • @aheze No, sorry. – Nicolai Henriksen Dec 28 '21 at 15:18
5

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.
}
Edward Brey
  • 40,302
  • 20
  • 199
  • 253
  • 1
    Nice solution. Haven't tested for retain cycles but I personally preferred to use `weak var window` in place of `let window` and it is working well so far. – stef Feb 26 '21 at 10:53
4

Access the current window by receiving NSWindow.didBecomeKeyNotification:

.onReceive(NotificationCenter.default.publisher(for: NSWindow.didBecomeKeyNotification)) { notification in
    if let window = notification.object as? NSWindow {
        // ...
    }
}
Toto
  • 570
  • 4
  • 13
1

Getting a window from AppDelegate, SceneDelegate or keyWindow is not suitable for multi-window app.

Here's my solution:

import UIKit
import SwiftUI

struct WindowReader: UIViewRepresentable {
  let handler: (UIWindow?) -> Void

  @MainActor
  final class View: UIView {
    var didMoveToWindowHandler: ((UIWindow?) -> Void)

    init(didMoveToWindowHandler: (@escaping (UIWindow?) -> Void)) {
      self.didMoveToWindowHandler = didMoveToWindowHandler
      super.init(frame: .null)
      backgroundColor = .clear
      isUserInteractionEnabled = false
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
      fatalError("init(coder:) has not been implemented")
    }

    override func didMoveToWindow() {
      super.didMoveToWindow()
      didMoveToWindowHandler(window)
    }
  }

  func makeUIView(context: Context) -> View {
    .init(didMoveToWindowHandler: handler)
  }

  func updateUIView(_ uiView: View, context: Context) {
    uiView.didMoveToWindowHandler = handler
  }
}

extension View {
  func onWindowChange(_ handler: @escaping (UIWindow?) -> Void) -> some View {
    background {
      WindowReader(handler: handler)
    }
  }
}

// on your SwiftUI view side:

struct MyView: View {
  var body: some View {
    Text("")
      .onWindowChange { window in
        print(window)
      }
  }
}
Jinwoo Kim
  • 440
  • 4
  • 7
0

At first I liked the answer given by @Asperi, but when trying it in my own environment I found it difficult to get working due to my need to know the root view at the time I create the window (hence I don't know the window at the time I create the root view). So I followed his example, but instead of an environment value I chose to use an environment object. This has much the same effect, but was easier for me to get working. The following is the code that I use. Note that I have created a generic class that creates an NSWindowController given a SwiftUI view. (Note that the userDefaultsManager is another object that I need in most of the windows in my application. But I think if you remove that line plus the appDelegate line you would end up with a solution that would work pretty much anywhere.)

class RootViewWindowController<RootView : View>: NSWindowController {
    convenience init(_ title: String,
                     withView rootView: RootView,
                     andInitialSize initialSize: NSSize = NSSize(width: 400, height: 500))
    {
        let appDelegate: AppDelegate = NSApplication.shared.delegate as! AppDelegate
        let windowWrapper = NSWindowWrapper()
        let actualRootView = rootView
            .frame(width: initialSize.width, height: initialSize.height)
            .environmentObject(appDelegate.userDefaultsManager)
            .environmentObject(windowWrapper)
        let hostingController = NSHostingController(rootView: actualRootView)
        let window = NSWindow(contentViewController: hostingController)
        window.setContentSize(initialSize)
        window.title = title
        windowWrapper.rootWindow = window
        self.init(window: window)
    }
}

final class NSWindowWrapper: ObservableObject {
    @Published var rootWindow: NSWindow? = nil
}

Then in my view where I need it (in order to close the window at the appropriate time), my struct begins as the following:

struct SubscribeToProFeaturesView: View {
    @State var showingEnlargedImage = false
    @EnvironmentObject var rootWindowWrapper: NSWindowWrapper

    var body: some View {
        VStack {
            Text("Professional Version Upgrade")
                .font(.headline)
            VStack(alignment: .leading) {

And in the button where I need to close the window I have

self.rootWindowWrapper.rootWindow?.close()

It's not quite as clean as I would like it to be (I would prefer to have a solution where I did just say self.rootWindow?.close() instead of requiring the wrapper class), but it isn't bad and it allows me to create the rootView object before I create the window.

Steven W. Klassen
  • 1,401
  • 12
  • 26
0

Instead of ProjectName_App use old fashioned AppDelegate approach as app entry point.

@main
final class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        ...
    }
}

Then pass window as environment object. For example:

struct WindowKey: EnvironmentKey {
    static let defaultValue: UIWindow? = nil
}

extension EnvironmentValues {
    var window: WindowKey.Value {
        get { return self[WindowKey.self] }
        set { self[WindowKey.self] = newValue }
    }
}

final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        
        window = UIWindow(windowScene: windowScene)
        
        let rootView = RootView()
            .environment(\.window, window)
        
        window?.rootViewController = UIHostingController(rootView: rootView)
        window?.makeKeyAndVisible()
    }
}

And use it when need it.

struct ListCell: View {
    @Environment(\.window) private var window

    var body: some View {
        Rectangle()
            .onTapGesture(perform: share)
    }

    private func share() {
        let vc = UIActivityViewController(activityItems: [], applicationActivities: nil)
        window?.rootViewController?.present(vc, animated: true)
    }
}
Ace Rodstin
  • 132
  • 1
  • 6
-1

2022, macOS only

Maybe not best solution, but works well for me and enough universal for almost any situation

Usage:

someView()
    .wndAccessor {
        $0?.title = String(localized: "This is a new window title")
    }

extension code:

import SwiftUI

@available(OSX 11.0, *)
public extension View {
    func wndAccessor(_ act: @escaping (NSWindow?) -> () )
        -> some View {
            self.modifier(WndTitleConfigurer(act: act))
    }
}

@available(OSX 11.0, *)
struct WndTitleConfigurer: ViewModifier {
    let act: (NSWindow?) -> ()
    
    @State var window: NSWindow? = nil
    
    func body(content: Content) -> some View {
        content
            .getWindow($window)
            .onChange(of: window, perform: act )
    }
}

//////////////////////////////
///HELPERS
/////////////////////////////

// Don't use this:
// Usage:
//.getWindow($window)
//.onChange(of: window) { _ in
//    if let wnd = window {
//        wnd.level = .floating
//    }
//}

@available(OSX 11.0, *)
private extension View {
    func getWindow(_ wnd: Binding<NSWindow?>) -> some View {
        self.background(WindowAccessor(window: wnd))
    }
}

@available(OSX 11.0, *)
private struct WindowAccessor: NSViewRepresentable {
    @Binding var window: NSWindow?
    
    public func makeNSView(context: Context) -> NSView {
        let view = NSView()
        DispatchQueue.main.async {
            self.window = view.window
        }
        return view
    }
    
    public func updateNSView(_ nsView: NSView, context: Context) {}
}
Andrew_STOP_RU_WAR_IN_UA
  • 9,318
  • 5
  • 65
  • 101