How to use window content size with default placement

We cover three ways we can use default window placement when opening a new window. We can use the size of the new window to ensure that it doesn’t overlap an existing one.

I learned something new about window placement while (remotely) attending an Apple Developer event recently.

I’ve been using default placement in my apps for a while now, but I have not been passing a size.

if let mainWindow = context.windows.first {
  return WindowPlacement(.trailing(mainWindow), size: size)
}

This tells the system two things.

  1. Open the new window relative to our main window
  2. This is the size of our new window.

visionOS will use these two bits of data to calculate an initial placement for the new window.

Example 1:

The initial size of the view is based on the content. This new window will be size and placed relative to the width and height of the text. The magic here is sizeThatFits(.unspecified). Notice that we don’t have a default size or a frame in this view.

WindowGroup(id: "PinkFlower") {
    VStack {
        Text("🌸")
        Text("🌸🌸🌸🌸🌸")
        Text("🌸")
    }
    .font(.system(size: 128))
}
.windowResizability(.contentMinSize)
.defaultWindowPlacement { content, context in
    let size = content.sizeThatFits(.unspecified)
    if let mainWindow = context.windows.first {
        return WindowPlacement(.trailing(mainWindow), size: size)
    }
    return WindowPlacement(.none)
}

Example 2:

Using a hard-coded frame size on the view, then using sizeThatFits(.unspecified) to calculate the size of the window.

WindowGroup(id: "RedFlower") {
    VStack {
        Text("🌺🌺🌺")
        Text("🌺🌺🌺")
        Text("🌺🌺🌺")
    }
    .font(.system(size: 128))
    .frame(width: 600, height: 500)
}
.windowResizability(.contentMinSize)
.defaultWindowPlacement { content, context in
    let size = content.sizeThatFits(.unspecified)
    if let mainWindow = context.windows.first {
        return WindowPlacement(.trailing(mainWindow), size: size)
    }
    return WindowPlacement(.none)
}

Example 3:

Have you ever opened a window only to have the tab bar or an ornament overlapping the origin window?

Video demo using a before and after concept that addresses overlapping ornaments.

We can hack around this by passing in a size that is larger than our window.

In this example, we set a min and max frame on the view, then used a defaultSize on the window. We pass a larger size to the window placement to account for the size of the ornaments.

WindowGroup(id: "YellowFlower") {
    
    TabView {...}
    .frame(minWidth: 500, maxWidth: 700, minHeight: 400, maxHeight: 700)
    
}
.windowResizability(.contentSize)
.defaultSize(CGSize(width: 600, height: 500))
.defaultWindowPlacement { content, context in
    let size = CGSize(width: 700, height: 500)
    if let mainWindow = context.windows.first {
        return WindowPlacement(.trailing(mainWindow), size: size)
    }
    return WindowPlacement(.none)
}

Note: this works well on visionOS 2 at the end of 2024, but in the future Apple may update these APIs to account for ornaments.

Video demo

Video demo showing three windows opening relative the an existing window, without any overlapping elements.

Sample code is available in Garden16 in Step Into Example Projects

Support our work so we can continue to bring you new examples and articles.

Download the Xcode project with this and many more examples from Step Into Vision.
Some examples are provided as standalone Xcode projects. You can find those here.

Questions or feedback?