Set Content View size/position upon retuning from navigation stack destination view

I have a program that utilizes a Navigation Stack. I want each view to be centered on the screen and be a specific size. I accomplished this with some position logic which sets the views frame size and origin. There is unique position logic for each of the 3 view sizes, the Content View and 2 navigation destination views. When the Content View is first displayed the associated position logic code runs and the view is the correct size and centered. The same is true every time one of the 2 navigation destination views is displayed. Unfortunately, when I return to the Content View from a navigation destination view the position logic does not run again and the Content View is now the same size and position as the previous navigation destination view. How do I resolve this problem? Below is the position logic code associated with the Content View and how it is called.

@main
struct TableViewTestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .hostingWindowPositionHome(
                    screen: .main
                )
        }
    }
}

extension View {

    func hostingWindowPositionHome(
        screen: NSScreen? = nil
    ) -> some View {
        modifier(
            WindowPositionModifierHome(
                screen: screen
            )
        )
    }
}
private struct WindowPositionModifierHome: ViewModifier {

    let screen: NSScreen?
    func body(content: Content) -> some View {
        content.background(
            HostingWindowFinderHome {
                $0?.setPositionHome(in: screen)
            }
        )
    }
}
private struct HostingWindowFinderHome: NSViewRepresentable { 
    
    var callback: (NSWindow?) -> ()
    func makeNSView(context: Self.Context) -> NSView {
        let view = NSView()
        DispatchQueue.main.async { self.callback(view.window) }
        return view
    }
    func updateNSView(_ nsView: NSView, context: Context) {
        DispatchQueue.main.async { self.callback(nsView.window) }
    }
}
extension NSWindow {

    func setPositionHome(in screen: NSScreen?) {
        let nsRectangle: NSRect = NSRect(x: 1055.0, y: 370.0, width: 450, height: 700)
        setFrame(nsRectangle, display: true)
    }
}


Answered by ChrisMH in 766879022

I finally figured out what to do. I created 3 WindowGroups in @main. One for the home view, one for the table view and one for the chart view. I then simplified the position logic as well as added logic to account for each view (they are all of the different size). I manually determined the origin and size of all three windows and assign the appropriate values in an NSRect which is used in the setFrame function of NSWindow. The position logic is assigned to each WindowGroup in @main. All works. Below is the updated code.

// @main code
struct WindowGroupsApp: App {
    
    var body: some Scene {
        WindowGroup ("Home") {
            ContentView()
                .hostingWindowPosition(window: "Home")
        }
        WindowGroup ("Table", id: "table", for: String.self) { $fundName in
            TableView(fundName: fundName!)
                .hostingWindowPosition(window: "Table")
        }
        WindowGroup ("Chart", id: "chart", for: String.self) { $fundName in
            ChartView(fundName: fundName!)
                .hostingWindowPosition(window: "Chart")
        }
    }
}

// position logic
extension View {

    func hostingWindowPosition(window: String) -> some View {
        modifier(
            WindowPositionModifier(window: window)
        )
    }
}

private struct WindowPositionModifier: ViewModifier {

    let window: String

    func body(content: Content) -> some View {
        content.background(
            HostingWindowFinder {
                $0?.setPosition(window: window)
            }
        )
    }
}

struct HostingWindowFinder: NSViewRepresentable {  // the system calls the methods at appropriate times
    
    var callback: (NSWindow?) -> ()
    func makeNSView(context: Self.Context) -> NSView {
        let view = NSView()
        DispatchQueue.main.async { self.callback(view.window) }
        return view
    }
    func updateNSView(_ nsView: NSView, context: Context) {
        DispatchQueue.main.async { self.callback(nsView.window) }
    }
}

extension NSWindow {

    func setPosition(window: String) {
        
        var nsRectangle: NSRect = NSRect(x: 100, y: 100, width: 100, height: 100)
        
        switch window {
        case "Home":
            nsRectangle = NSRect(x: 1055.0, y: 370.0, width: 450, height: 700)
        case "Table":
            nsRectangle = NSRect(x: 1055.0, y: 320.0, width: 450, height: 800)  // 390
        case "Chart":
            nsRectangle = NSRect(x: 680.0, y: 245.0, width: 1200, height: 950)
        default: print("problem in window logic set position")
        }
        setFrame(nsRectangle, display: true)
    }
}

// content 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: "Table1")
                    } label: {
                        Text("Table")
                            .font(Font.custom("Arial", size: 14.0))
                            .foregroundColor(Color.blue)
                            .background(Color.clear)
                            .padding(.leading, 35)
                    }
                    .focusable(false)
                    .buttonStyle(.link)
                    
                    Button {
                        openWindow(id: "chart", value: "Chart1")
                    } label: {
                        Text("Chart - 1 Year")
                            .font(Font.custom("Arial", size: 14.0))
                            .foregroundColor(Color.blue)
                            .background(Color.clear)
                            .padding(.leading, 35)
                    }
                    .focusable(false)
                    .buttonStyle(.link)
                    
                    Button {
                        openWindow(id: "chart", value: "Chart5")
                    } label: {
                        Text("Chart - 5 Years")
                            .font(Font.custom("Arial", size: 14.0))
                            .foregroundColor(Color.blue)
                            .background(Color.clear)
                            .padding(.leading, 35)
                    }
                    .focusable(false)
                    .buttonStyle(.link)
                    
                } // end v stack
                
            } // end zstack
            
        } // end v stack
        
        .frame(width: 450, height: 600, alignment: .topLeading)
    } // end some view
}

A question first. I don't understand your code:

    func setPositionHome(in screen: NSScreen?) {
        let nsRectangle: NSRect = NSRect(x: 1055.0, y: 370.0, width: 450, height: 700)
        setFrame(nsRectangle, display: true)
    }

What is the in parameter used for ?

Are you sure you need the dispatchQueue ? It seems to be calles from main thread anyway.

you return view before the dispatch is executed.

or you should wait for the dispatch to be completed before returning.

could you try to remove the dispatch ? Or wait for a small time (1/100 s) before return from the func ?

I updated the HostingWindowFinder*** struct (where *** is replaced with Home, Table or Chart). With this code the home view (content view) is correctly positioned and the right size. When I select a destination view (chart or table) it appears as if the origin of the home view does not change and that the window size increases to just fit the table or the chart. When I return to the Content View it goes back to the original size/position. When I step through the code, the setPosition function for the chart view for example is executed but it does not appear that the setFrame call is doing anything. I am clearly misunderstanding something. Probably something very obvious. Below is the updated HostingWindowFinderHome code.

private struct HostingWindowFinderHome: NSViewRepresentable {
   var callback: (NSWindow) -> ()
   func makeNSView(context: Self.Context) -> NSView {
       let view = BridgingViewHome()
       view.callback = callback
       return view
   }
   func updateNSView(_ nsView: NSView, context: Context) {}
}
private class BridgingViewHome: NSView {
   var callback: ((NSWindow) -> ())?
   override func draw(_ dirtyRect: NSRect) {
       super.draw(dirtyRect)
       if let window = window {
           callback?(window)
       }
   }
}

After some additional testing, discovered that I just have to associate the code with the home view (ContentView). With this in place the home view initially displays centered and the correct size. When I go to a destination view (both are larger) the view stays centered and its size adjusts correctly. But when I return to the home view. the size/position of the view does not change. To get the home view to be the correct width and height initially I set the default size in the initial call to ContentView (see code below). So it makes sense to me that I all I need to do upon returning to the home view is reset the default size. Unfortunately I have not yet figured out how to do that. If. someone has any ideas I am all ears. If I figure it out, I will post the solution.

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

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

I finally figured out what to do. I created 3 WindowGroups in @main. One for the home view, one for the table view and one for the chart view. I then simplified the position logic as well as added logic to account for each view (they are all of the different size). I manually determined the origin and size of all three windows and assign the appropriate values in an NSRect which is used in the setFrame function of NSWindow. The position logic is assigned to each WindowGroup in @main. All works. Below is the updated code.

// @main code
struct WindowGroupsApp: App {
    
    var body: some Scene {
        WindowGroup ("Home") {
            ContentView()
                .hostingWindowPosition(window: "Home")
        }
        WindowGroup ("Table", id: "table", for: String.self) { $fundName in
            TableView(fundName: fundName!)
                .hostingWindowPosition(window: "Table")
        }
        WindowGroup ("Chart", id: "chart", for: String.self) { $fundName in
            ChartView(fundName: fundName!)
                .hostingWindowPosition(window: "Chart")
        }
    }
}

// position logic
extension View {

    func hostingWindowPosition(window: String) -> some View {
        modifier(
            WindowPositionModifier(window: window)
        )
    }
}

private struct WindowPositionModifier: ViewModifier {

    let window: String

    func body(content: Content) -> some View {
        content.background(
            HostingWindowFinder {
                $0?.setPosition(window: window)
            }
        )
    }
}

struct HostingWindowFinder: NSViewRepresentable {  // the system calls the methods at appropriate times
    
    var callback: (NSWindow?) -> ()
    func makeNSView(context: Self.Context) -> NSView {
        let view = NSView()
        DispatchQueue.main.async { self.callback(view.window) }
        return view
    }
    func updateNSView(_ nsView: NSView, context: Context) {
        DispatchQueue.main.async { self.callback(nsView.window) }
    }
}

extension NSWindow {

    func setPosition(window: String) {
        
        var nsRectangle: NSRect = NSRect(x: 100, y: 100, width: 100, height: 100)
        
        switch window {
        case "Home":
            nsRectangle = NSRect(x: 1055.0, y: 370.0, width: 450, height: 700)
        case "Table":
            nsRectangle = NSRect(x: 1055.0, y: 320.0, width: 450, height: 800)  // 390
        case "Chart":
            nsRectangle = NSRect(x: 680.0, y: 245.0, width: 1200, height: 950)
        default: print("problem in window logic set position")
        }
        setFrame(nsRectangle, display: true)
    }
}

// content 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: "Table1")
                    } label: {
                        Text("Table")
                            .font(Font.custom("Arial", size: 14.0))
                            .foregroundColor(Color.blue)
                            .background(Color.clear)
                            .padding(.leading, 35)
                    }
                    .focusable(false)
                    .buttonStyle(.link)
                    
                    Button {
                        openWindow(id: "chart", value: "Chart1")
                    } label: {
                        Text("Chart - 1 Year")
                            .font(Font.custom("Arial", size: 14.0))
                            .foregroundColor(Color.blue)
                            .background(Color.clear)
                            .padding(.leading, 35)
                    }
                    .focusable(false)
                    .buttonStyle(.link)
                    
                    Button {
                        openWindow(id: "chart", value: "Chart5")
                    } label: {
                        Text("Chart - 5 Years")
                            .font(Font.custom("Arial", size: 14.0))
                            .foregroundColor(Color.blue)
                            .background(Color.clear)
                            .padding(.leading, 35)
                    }
                    .focusable(false)
                    .buttonStyle(.link)
                    
                } // end v stack
                
            } // end zstack
            
        } // end v stack
        
        .frame(width: 450, height: 600, alignment: .topLeading)
    } // end some view
}

Set Content View size/position upon retuning from navigation stack destination view
 
 
Q