Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Change window size based on NavigationView in a SwiftUI macOS app

I'm using SwiftUI for a Mac app where the main window contains a NavigationView. This NavigationView contains a sidebar list. When an item in the sidebar is selected, it changes the view displayed in the detail view. The views presented in the detail view are different sizes which should cause the size of the window to change when they are displayed. However, when the detail view changes size the window does not change size to accommodate the new detail view.

How can I make the window size change according to the size of the NavigationView?

My example code for the app is below:

import SwiftUI

struct View200: View {
    var body: some View {
        Text("200").font(.title)
            .frame(width: 200, height: 400)
            .background(Color(.systemRed))
    }
}

struct View500: View {
    var body: some View {
        Text("500").font(.title)
            .frame(width: 500, height: 300)
            .background(Color(.systemBlue))
    }
}

struct ViewOther: View {
    let item: Int
    var body: some View {
        Text("\(item)").font(.title)
            .frame(width: 300, height: 200)
            .background(Color(.systemGreen))
    }
}

struct DetailView: View {

    let item: Int

    var body: some View {
        switch item {
        case 2:
            return AnyView(View200())
        case 5:
            return AnyView(View500())
        default:
            return AnyView(ViewOther(item: item))
        }
    }
}

struct ContentView: View {
    var body: some View {
        NavigationView {
             List {
                 ForEach(1...10, id: \.self) { index in
                     NavigationLink(destination: DetailView(item: index)) {
                         Text("Link \(index)")
                     }
                 }
             }
             .listStyle(SidebarListStyle())
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

And here is what the example app looks like when the detail view changes size:

screen recording

like image 795
wigging Avatar asked Dec 18 '19 04:12

wigging


2 Answers

Here is demo of possible approach that works. I did it on one different view, because you will need to redesign your solution to adopt it.

Demo

SwiftUI animate window frame

1) The view requiring window animated resize

struct ResizingView: View {
    public static let needsNewSize = Notification.Name("needsNewSize")

    @State var resizing = false
    var body: some View {
        VStack {
            Button(action: {
                self.resizing.toggle()
                NotificationCenter.default.post(name: Self.needsNewSize, object: 
                    CGSize(width: self.resizing ? 800 : 400, height: self.resizing ? 350 : 200))
            }, label: { Text("Resize") } )
        }
        .frame(minWidth: 400, maxWidth: .infinity, minHeight: 200, maxHeight: .infinity)
    }
}

2) Window's owner (in this case AppDelegate)

import Cocoa
import SwiftUI
import Combine

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    var window: NSWindow!
    var subscribers = Set<AnyCancellable>()

    func applicationDidFinishLaunching(_ aNotification: Notification) {

        let contentView = ResizingView()

        window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), // just default
            styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
            backing: .buffered, defer: false)
        window.center()
        window.setFrameAutosaveName("Main Window")
        window.contentView = NSHostingView(rootView: contentView)
        window.makeKeyAndOrderFront(nil)

        NotificationCenter.default.publisher(for: ResizingView.needsNewSize)
            .sink(receiveCompletion: {_ in}) { [unowned self] notificaiton in
                if let size = notificaiton.object as? CGSize {
                    var frame = self.window.frame
                    let old = self.window.contentRect(forFrameRect: frame).size

                    let dX = size.width - old.width
                    let dY = size.height - old.height

                    frame.origin.y -= dY // origin in flipped coordinates
                    frame.size.width += dX
                    frame.size.height += dY
                    self.window.setFrame(frame, display: true, animate: true)
                }
            }
            .store(in: &subscribers)
    }
    ...
like image 58
Asperi Avatar answered Oct 17 '22 05:10

Asperi


Asperi's answer works for me, but the animation is not working on Big Sur 11.0.1, Xcode 12.2. Thankfully, the animation works if you wrap it in an NSAnimationContext:

NSAnimationContext.runAnimationGroup({ context in
    context.timingFunction = CAMediaTimingFunction(name: .easeIn)
    window!.animator().setFrame(frame, display: true, animate: true)
}, completionHandler: {
})

Also it should be mentioned that ResizingView and window don't have to be initialized inside AppDelegate; you can continue using SwiftUI's App struct:

@main
struct MyApp: App {

    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            ResizingView()
        }
    }
}

class AppDelegate: NSObject, NSApplicationDelegate {

    var window: NSWindow?
    var subscribers = Set<AnyCancellable>()

    func applicationDidBecomeActive(_ notification: Notification) {
        self.window = NSApp.mainWindow
    }

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        setupResizeNotification()
    }

    private func setupResizeNotification() {
        NotificationCenter.default.publisher(for: ResizingView.needsNewSize)
            .sink(receiveCompletion: {_ in}) { [unowned self] notificaiton in
                if let size = notificaiton.object as? CGSize, self.window != nil {
                    var frame = self.window!.frame
                    let old = self.window!.contentRect(forFrameRect: frame).size
                    let dX = size.width - old.width
                    let dY = size.height - old.height
                    frame.origin.y -= dY // origin in flipped coordinates
                    frame.size.width += dX
                    frame.size.height += dY
                    NSAnimationContext.runAnimationGroup({ context in
                        context.timingFunction = CAMediaTimingFunction(name: .easeIn)
                        window!.animator().setFrame(frame, display: true, animate: true)
                    }, completionHandler: {
                    })
                }
            }
            .store(in: &subscribers)
    }
}
like image 32
Dominic Holmes Avatar answered Oct 17 '22 04:10

Dominic Holmes