9

I want to make a parallax background view, where the image behind the UI stays nearly still as the window moves around on the screen. To do this on macOS, I want to get the window's coordinates. How do I get the window's coordinates?


I ask this because I can't find anywhere that says how to do this:

As I listed, I found that all these either didn't relate to my issue, or only reference the coordinates within the window, but not the window's coordinates within the screen. Some mention ways to dip into AppKit, but I want to avoid that if possible.

The closest I got was trying to use a GeometryReader like this:

GeometryReader { geometry in
    Text(verbatim: "\(geometry.frame(in: .global))")
}

but the origin was always (0, 0), though the size did change as I adjusted the window.


What I was envisioning was something perhaps like this:

public struct ParallaxBackground<Background: View>: View {
    var background: Background

    @Environment(\.windowFrame)
    var windowFrame: CGRect

    public var body: some View {
        background
            .offset(x: windowFrame.minX / 10,
                    y: windowFrame.minY / 10)
    }
}

but \.windowFrame isn't real; it doesn't point to any keypath on EnvironmentValues. I can't find where I would get such a value.

Ky -
  • 30,724
  • 51
  • 192
  • 308

2 Answers2

2

As of today we have macOS 12 widely deployed/installed and SwiftUI has not gained a proper model for the macOS window. And from what I learned so far about macOS 13, there won't be a SwiftUI model for the window coming either.

Today (since macOS 11) we are not opening windows in the AppDelegate anymore but are now defining windows using the WindowGroup scene modifiers:

@main
struct HandleWindowApp: App {
    var body: some Scene {
        WindowGroup(id: "main") {
            ContentView()
        }
    }
}

But there is no standard way to control or access the underlying window (e.g. NSWindow). To do this multiple answers on stackoverflow suggest to use a WindowAccessor which installs a NSView in the background of the ContentView and then accessing its window property. I also wrote my version of it to control the placement of windows. In your case, it is sufficient to get a handle to the NSWindow instance and then observe the NSWindow.didMoveNotification. It will get called whenever the window did move.

If your app is using only a single window (e.g. you somehow inhibit that multiple windows can be created by the user), you can even observe the frames positions globally:

NotificationCenter.default
    .addObserver(forName: NSWindow.didMoveNotification, object: nil, queue: nil) { (notification) in
        if let window = notification.object as? NSWindow,
           type(of: window).description() == "SwiftUI.SwiftUIWindow"
        {
            print(window.frame)
        }
    }
Eric Aya
  • 69,473
  • 35
  • 181
  • 253
pd95
  • 1,999
  • 1
  • 20
  • 33
1

If you want the window frame:

The SceneDelegate keeps track of all the windows, so you can use it to make an EnvironmentObject with a reference to their frames and pass that to your View. Update the environment object values in the delegate method: func windowScene(_ windowScene: UIWindowScene, didUpdate previousCoordinateSpace: UICoordinateSpace, ...

If it's a one window app, it's much more straight forward. You could use UIScreen.main.bounds (if full screen) or a computed variable in you view:

var frame: CGRect { (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window?.frame ?? .zero }

But if you are looking for the frame of the view in the window, try something like this:

struct ContentView: View {
  @State var frame: CGRect = .zero

  var orientationChangedPublisher = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)

  var body: some View {
    VStack {
      Text("text frame georeader \(frame.debugDescription)")
    }
    .background(GeometryReader { geometry in
      Color.clear // .edgesIgnoringSafeArea(.all) // may need depending
        .onReceive(self.orientationChangedPublisher.removeDuplicates()) { _ in
          self.frame = geometry.frame(in: .global)
      }
    })
  }
} 

But having said all that, usually you don't need an absolute frame. Alignment guides let you place things relative to each other.

// For macOS App, using Frame Changed Notification and passing as Environment Object to SwiftUI View

class WindowInfo: ObservableObject {
  @Published var frame: CGRect = .zero
}

@NSApplicationMain

class AppDelegate: NSObject, NSApplicationDelegate {

  var window: NSWindow!

  let windowInfo = WindowInfo()

  func applicationDidFinishLaunching(_ aNotification: Notification) {
    // Create the SwiftUI view that provides the window contents.
    let contentView = ContentView()
      .environmentObject(windowInfo)

    // Create the window and set the content view. 
    window = NSWindow(
        contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
        styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
        backing: .buffered, defer: false)
    window.center()
    window.setFrameAutosaveName("Main Window")
    window.contentView = NSHostingView(rootView: contentView)
    window.contentView?.postsFrameChangedNotifications = true
    window.makeKeyAndOrderFront(nil)

    NotificationCenter.default.addObserver(forName: NSView.frameDidChangeNotification, object: nil, queue: nil) { (notification) in
      self.windowInfo.frame = self.window.frame
    }
  }
struct ContentView: View {
  @EnvironmentObject var windowInfo: WindowInfo

  var body: some View {
    Group  {
      Text("Hello, World! \(windowInfo.frame.debugDescription)")
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
  }
}
Cenk Bilgen
  • 1,330
  • 9
  • 8
  • 1
    As I said at the top of the question and in its tags, this is for a macOS app. Though it's neat to think about this in iOS, can you translate this from UIKit to AppKit? – Ky - Jun 11 '20 at 16:26
  • I expected you could swap out the OrientationChanged notification with `NSView.frameDidChangeNotification`, but it didn't work. However, in a round-about way you can still use it to get the frame info and pass it to the SwiftUI View (updated answer). – Cenk Bilgen Jun 11 '20 at 17:51
  • @CenkBilgen This is great, but updates only come for the windows dimensions when the frame is resized, is there a way to get notifications for when the frame/window is just moved? Basically constant tracking of where the window is before/after movements? Alternatively, is there a way to get the windows current position at any time, like on a button press? If I press a button to print its position, then move the window and press it again, I get the previous window position until I resize the window and re press. – 956MB Jul 23 '21 at 14:53