swiftmacosswiftuinswindow

How do I reset view frame origin, width & height when returning from a navigation destination


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
        }
    }
}

Solution

  • 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)
        } 
    }