1

I'm building a macOS 13 app using SwiftUI. The app has two-column navigation by using the NavigationSplitView which gives a sidebar and detail view. The detail views are different sizes so I would like the window to change size based on the size of each detail view.

In the example below, the detail views are AppleView, KiwiView, and PeachView. The AppleView has a size of 400x300 and the KiwiView is 300x200. When I run the app, the window does not adjust its size when the detail view changes. I tried to wrap the navigation view in a VStack but that did not help. Does anyone know how I can get the app's window to adjust size based on the selected detail view?

import SwiftUI

struct AppleView: View {
    var body: some View {
        Text("Apple View ")
            .font(.title)
            .frame(width: 400, height: 300)
            .background(.red)
    }
}

struct KiwiView: View {
    var body: some View {
        Text("Kiwi View ")
            .font(.title)
            .frame(width: 300, height: 200)
            .background(.green)
    }
}

struct PeachView: View {
    var body: some View {
        Text("Peach View ")
            .font(.title)
            .background(.pink)
    }
}

enum Fruit: String, CaseIterable {
    case apple = "Apple"
    case kiwi = "Kiwi"
    case peach = "Peach"
}

struct ContentView: View {
    
    @State private var selectedFruit: Fruit = .apple
    
    var body: some View {
        NavigationSplitView {
            List(Fruit.allCases, id: \.self, selection: $selectedFruit) { fruit in
                Text(fruit.rawValue)
            }
        } detail: {
            switch selectedFruit {
            case .apple:
                AppleView()
            case .kiwi:
                KiwiView()
            case .peach:
                PeachView()
            }
        }
    }
}

screenshot

I also tried setting the .windowResizability() of the main window group but that didn't fix the problem.


import SwiftUI

@main
struct ExampleApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .windowResizability(.contentSize)
    }
}
wigging
  • 8,492
  • 12
  • 75
  • 117
  • Does [this](https://stackoverflow.com/questions/74410407/can-you-dynamically-change-window-size-of-macos-app-built-with-swiftui) answer your question? – newb Nov 22 '22 at 08:49
  • @newb How would your suggestion be applied to a `NavigationSplitView`? – wigging Nov 22 '22 at 15:57

1 Answers1

0

sorry for the late reply.. It would apply to NavigationSplitView just the same. Since you want to have fixed sizes, I would add frames to all the sub-Views for consistent behaviour, so in this example the PeachView and the List

and then you simply modify your ContentView accordingly:

struct ContentView: View {
    
    @State private var selectedFruit: Fruit = .apple
    @State private var window: NSWindow?
    
    var body: some View {
        NavigationSplitView {
            List(Fruit.allCases, id: \.self, selection: $selectedFruit) { fruit in
                Text(fruit.rawValue)
            }
            .frame(width: 100)
        } detail: {
            switch selectedFruit {
            case .apple:
                AppleView()
            case .kiwi:
                KiwiView()
            case .peach:
                PeachView()
            }
        }
        .background(WindowAccessor(window: $window))
        .onChange(of: selectedFruit) { newValue in
            if newValue == .apple {
                window?.setFrame(NSRect(x: 600, y: 400, width: 500, height: 600), display: true)
            } else if newValue == .kiwi {
                window?.setFrame(NSRect(x: 600, y: 400, width: 400, height: 500), display: true)
            } else if newValue == .peach {
                window?.setFrame(NSRect(x: 600, y: 400, width: 350, height: 400), display: true)
            }
        }
    }
}

And then add the WindowAccessor :

struct WindowAccessor: NSViewRepresentable {
    @Binding var window: NSWindow?

    func makeNSView(context: Context) -> NSView {
        let view = NSView()
        DispatchQueue.main.async {
            self.window = view.window
        }
        return view
    }

    func updateNSView(_ nsView: NSView, context: Context) {}
}

For full access of the Window across the app you might also wanna look into accessing it with App/Scene Delegate, as explained here

EDIT: Addressing shifting behaviour

You can position the Window anywhere you want, specified by the x and y coordinates of the NSRect you create in window?.setFrame. Since you didnt say what kind of positioning you'd like, I now 'pinned' the window to the top-left-corner, by accessing the underlying screen as follows:

.onChange(of: selectedFruit) { newValue in
    if newValue == .apple {
        window?.setFrame(NSRect(x: 0, y: window?.screen?.frame.maxY ?? 500, width: 500, height: 600), display: true)
    } else if newValue == .kiwi {
        window?.setFrame(NSRect(x: 0, y: window?.screen?.frame.maxY ?? 500, width: 400, height: 500), display: true)
    } else if newValue == .peach {
        window?.setFrame(NSRect(x: 0, y: window?.screen?.frame.maxY ?? 500, width: 400, height: 300), display: true)
    }
}

For more complex positions you just need to calculate frame dimensions and origin, according to your wanted behaviour. The detail view beeing clipped is fixed by adjusting frame size.

Further down the line you might find this helpful when dealing with MacOS saving the last position and size of the window.

newb
  • 172
  • 1
  • 9
  • This does not work well. The window jumps around when the detail view changes. And the detail view gets clipped by the window. – wigging Dec 09 '22 at 03:05