I have a program that utilizes a navigation stack with 2 navigation destinations. I could not figure out how to center each view frame on the screen and have the width and height adjust for the specific view being displayed (all 3 have a different frame width and height, the 2 navigation destinations views are larger than the home view).
So I found the code below online. The link is listed below. I have integrated the code into mine. It allows me to have the initial home view frame display centered with the desired size.
When I go to a navigation destination view the view frame size adjust properly and is centered. But when I go back to the home view the frame size/position does not change. If I then go to the other navigation destination view, the frame size changes only if it is larger. So the view frame size/position ends up at what ever was the largest view size displayed but always centered.
In the code that I found online it sets the frame origin in an extension for NSWindow. To get the home view to display with the desired width and height I had to set the default in @main as follows: .defaultSize(width: 450, height: 700)
. Since this is setting a default size it makes sense that I should be able up returning to the home view reset the frame size (and origin if required) to the default.
I created a function setHomePosition
which was added to the NSWindow extension (see code below) and thought I could run it onappear
in the home view (ContentView) but Xcode does not find the function. I think because it is an extension to NSWindow, not View.
In looking at the code of the home view (contentview) and the 2 navigation destination views I realized that each of the detination views contains a .frame(), setting the width and height set, but the home view does not. I think it is set with the .default size line of code in @main. So it appears that if I could determine how to set the frame of the home view upon returning to it I might have a solution. If someone could point me in the right direction it would be appreciated.
https://gist.github.com/ABridoux/b935c21c7ead92033d39b357fae6366b
@main
struct TableViewTestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.hostingWindowPosition(
vertical: .center,
horizontal: .center,
screen: .main
)
}
.defaultSize(width: 450, height: 700)
}
}
extension NSWindow {
struct Position {
static let defaultPadding: CGFloat = 16
var vertical: Vertical
var horizontal: Horizontal
var padding = Self.defaultPadding
}
}
extension NSWindow.Position {
enum Horizontal {
case left, center, right
}
enum Vertical {
case top, center, bottom
}
}
extension View {
func hostingWindowPosition(
vertical: NSWindow.Position.Vertical,
horizontal: NSWindow.Position.Horizontal,
padding: CGFloat = NSWindow.Position.defaultPadding,
screen: NSScreen? = nil
) -> some View {
modifier(
WindowPositionModifier(
position: NSWindow.Position(
vertical: vertical,
horizontal: horizontal,
padding: padding
),
screen: screen
)
)
}
}
private struct WindowPositionModifier: ViewModifier {
let position: NSWindow.Position
let screen: NSScreen?
func body(content: Content) -> some View {
content.background(
HostingWindowFinder {
$0.setPosition(position, in: screen)
}
)
}
}
private struct HostingWindowFinder: NSViewRepresentable {
var callback: (NSWindow) -> ()
func makeNSView(context: Self.Context) -> NSView {
let view = BridgingView()
view.callback = callback
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
}
private class BridgingView: NSView {
var callback: ((NSWindow) -> ())?
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
if let window = window {
callback?(window)
}
}
}
extension NSWindow {
func setPosition(_ position: Position, in screen: NSScreen?) {
guard let visibleFrame = (screen ?? self.screen)?.visibleFrame else { return }
let origin = position.value(forWindow: frame, inScreen: visibleFrame)
setFrameOrigin(origin)
}
// I added this function
func setHomePosition() {
let nsRectangle: NSRect = NSRect(x: 1055.0, y: 370.0, width: 450, height: 700)
setFrame(nsRectangle, display: true)
}
}
extension NSWindow.Position {
func value(forWindow windowRect: CGRect, inScreen screenRect: CGRect) -> CGPoint {
let xPosition = horizontal.valueFor(
screenRange: screenRect.minX..<screenRect.maxX,
width: windowRect.width,
padding: padding
)
let yPosition = vertical.valueFor(
screenRange: screenRect.minY..<screenRect.maxY,
height: windowRect.height,
padding: padding
)
return CGPoint(x: xPosition, y: yPosition)
}
}
extension NSWindow.Position.Horizontal {
func valueFor(
screenRange: Range<CGFloat>,
width: CGFloat,
padding: CGFloat)
-> CGFloat {
switch self {
case .left: return screenRange.lowerBound + padding
case .center:
return (screenRange.upperBound + screenRange.lowerBound - width) / 2
case .right: return screenRange.upperBound - width - padding
}
}
}
extension NSWindow.Position.Vertical {
func valueFor(
screenRange: Range<CGFloat>,
height: CGFloat,
padding: CGFloat)
-> CGFloat {
switch self {
case .top: return screenRange.upperBound - height - padding
case .center: return (screenRange.upperBound + screenRange.lowerBound - height) / 2
case .bottom: return screenRange.lowerBound + padding
}
}
}
After some additional research I determined that I could get the desired results by creating a WindowGroup in @main for each window size/origin I wanted. So I ended up with a home, table and chart window group. To get the size/origin I wanted for each window group (all are different) I modified the setPosition NSWindow extension above, hard coding an nsRectangle for each window group and assigning it with setFrame. This meant creating a position logic file for each window. The navigation stack is no longer used and in its place I have a series of buttons whose action is openWindow(id:value:) and of course using multiple window groups means when a button is selected an additional window is displayed.
//updated nswindow extension in position logic for home window group
func setPositionHome(_ position: Position, in screen: NSScreen?) {
let nsRectangle: NSRect = NSRect(x: 1055.0, y: 370.0, width: 450, height: 700)
setFrame(nsRectangle, display: true)
}
// updated @main
struct WindowGroupsApp: App {
var body: some Scene {
WindowGroup ("Home") {
ContentView()
.hostingWindowPositionHome(
vertical: .center,
horizontal: .center,
screen: .main
)
}
WindowGroup ("Table", id: "table", for: String.self) { $fundName in
TableView(fundName: fundName!)
.hostingWindowPositionTable(
vertical: .center,
horizontal: .center,
screen: .main
)
}
WindowGroup ("Chart", id: "chart", for: String.self) { $fundName in
ChartView(fundName: fundName!)
.hostingWindowPositionChart(
vertical: .center,
horizontal: .center,
screen: .main
)
}
}
}
// home view
struct ContentView: View {
@Environment (\.openWindow) private var openWindow
var body: some View {
VStack(alignment: .leading) {
ZStack (alignment: .leading) {
RoundedRectangle(cornerRadius: 10.0)
.fill(Color.white)
.frame(width: 200, height: 80)
.padding(.leading, 20)
VStack(alignment: .leading, spacing: 10) {
Button {
openWindow(id: "table", value: "Table")
} label: {
Text("Table")
.font(Font.custom("Arial", size: 14.0))
.foregroundColor(Color.blue)
.background(Color.clear)
.padding(.leading, 35)
}
.focusable(false)
.buttonStyle(PlainButtonStyle())
Button {
openWindow(id: "chart", value: "Chart")
} label: {
Text("Chart")
.font(Font.custom("Arial", size: 14.0))
.foregroundColor(Color.blue)
.background(Color.clear)
.padding(.leading, 35)
}
.focusable(false)
.buttonStyle(PlainButtonStyle())
}
}
}
.frame(width: 450, height: 600, alignment: .topLeading)
}
}