Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I get the window coordinates in SwiftUI?

Tags:

macos

swiftui

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:

    • SwiftUI window coordinates, SwiftUI window location, SwiftUI window get frame, SwiftUI get window, SwiftUI macOS window coordinates, SwiftUI macOS window location, SwiftUI macOS window get frame, SwiftUI macOS get window
  • 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 coordinates
    • Creating a macOS App — SwiftUI Tutorials - I was hoping Apple would have mentioned windowing in this, but it's not mentioned at all, aside from saying that you can preview the main window's contents in an Xcode preview pane
    • Fruitless searches: SwiftUI window coordinates, SwiftUI window location, SwiftUI window get frame
  • Other SO questions:

    • How to access own window within SwiftUI view? - I was optimistic that this would have an answer which would give me a SwiftUI API to access the window, but instead it uses a shim to access the AppKit window representation.
    • Define macOS window size using SwiftUI - Similar hopes as the above question, but this time the answer was just to read the frame of the content view, which again, always has a (0, 0) origin
    • SwiftUI coordinate space - I was hoping this answer would let me know how to transform the coordinates given by GeometryReader into screen coordinates, but unfortunately the coordinates are again constrained to within the window
  • Elsewhere on the web:

    • SwiftUI for Mac - Part 2 by TrozWare - I was hoping that this would give me some tips for using SwiftUI on Mac, such as interacting with windows, since most tutorials focus on iOS/iPadOS. Unfortunately, although it has lots of good information about how SwiftUI works with windows, it has no information on interacting with nor parsing those windows, themselves
    • SwiftUI Layout System by Alexander Grebenyuk - Was hoping for window layout within the screen, but this is all for full-screen iOS apps
    • SwiftUI by Example by Hacking with Swift - Was hoping for an example for how to get the position of a window, but it seems windows aren't really mentioned at all in the listed examples

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.

like image 497
Ky. Avatar asked Jun 07 '20 22:06

Ky.


People also ask

How do I get Windows on SwiftUI?

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.

What is coordinate space in SwiftUI?

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:) .

How do I set positions in SwiftUI?

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.

What is GeometryReader SwiftUI?

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.


2 Answers

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)
    }
  }
}
like image 125
Cenk Bilgen Avatar answered Nov 12 '22 08:11

Cenk Bilgen


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)
        }
    }
like image 45
pd95 Avatar answered Nov 12 '22 09:11

pd95