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:
Google searches which helped me find the following results:
Apple Developer Documentation:
GeometryReader
- I had hoped that this would contain an API to give me the frame in system coordinate space, but it seems all the approaches it contains only reference within-the-window coordinatesOther SO questions:
(0, 0)
originGeometryReader
into screen coordinates, but unfortunately the coordinates are again constrained to within the windowElsewhere on the web:
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.
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. Finally, use the window in your view. struct MyView: View { @EnvironmentObject private var appData: AppData // Use appData.
Overview. CoordinateSpace allows a set of X, Y coordinates to have context on if they are relative to the frame's parent, or absolute to the device screen. It is also possible to define a custom coordinate space on a view with coordinateSpace(name:) .
SwiftUI gives us two ways of positioning views: absolute positions using position() , and relative positions using offset() . They might seem similar, but once you understand how SwiftUI places views inside frames the underlying differences between position() and offset() become clearer.
SwiftUI's GeometryReader allows us to use its size and coordinates to determine a child view's layout, and it's the key to creating some of the most remarkable effects in SwiftUI.
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)
}
}
}
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)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With