Animating NSWindow based on SwiftUI View Transition

70 Views Asked by At

I have a MacOS MenuBar app with a custom rounded NSWindow:

class RoundedWindow: NSWindow {
    override var contentView: NSView? {
        didSet {
            guard let contentView = contentView else { return }
            contentView.wantsLayer = true
            contentView.layer?.cornerRadius = 10
            contentView.layer?.masksToBounds = true
        }
    }
}

class ClearBackgroundHostingController<Content>: NSHostingController<Content> where Content: View {
    override func loadView() {
        self.view = NSView()
        self.view.wantsLayer = true
        self.view.layer?.backgroundColor = NSColor.clear.cgColor
    }
}

And a function in my AppDelegate to adjust the window size

func adjustWindowSize(to size: CGSize) {
        guard let window = windowController.window else { return }
        var newFrame = window.frame
        newFrame.size = size

        // Calculate the new Y position to keep the window top aligned.
        let newY = window.frame.maxY - size.height
        newFrame.origin.y = newY

        NSAnimationContext.runAnimationGroup({ context in
            context.duration = 10.0
            context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
            window.animator().setFrame(newFrame, display: true)
        }, completionHandler: nil)
    }

I'm keeping track of whether or not the preferences window is open, and trying to update and animate the size transition accordingly:

enter image description here

static let largeSize = CGSize(width: 512, height: 288)
static let smallSize = CGSize(width: 220, height: 220)
@State var size: CGSize = Self.smallSize

var body: some View {
    ZStack {
        if appDelegate.showingPreferences {
            PreferencesView(appDelegate: appDelegate)
        } else {
            if viewModel.isVPNAvailable || !viewModel.isVPNAvailable {
                mainContentView
            }
            
            if !viewModel.isVPNAvailable {
                vpnNotAvailableView
            }
        }
    }
    .onChange(of: appDelegate.showingPreferences) { _ in
        withAnimation(.easeInOut(duration: 1)) {
            let newSize = size == Self.largeSize ? Self.smallSize : Self.largeSize
            size = newSize
            appDelegate.adjustWindowSize(to: newSize)
        }
    }

    .onReceive(appDelegate.vpnStatusPublisher.receive(on: DispatchQueue.main)) { newStatus in
        actionInProgress = false
        switch newStatus {
        case .connected:
            appDelegate.vpnIsConnected = true
        case .connecting, .disconnecting, .disconnected:
            appDelegate.vpnIsConnected = false
        }
    }
    .onChange(of: appDelegate.showingPreferences) { _ in
        withAnimation(.easeInOut(duration: 1)) { // Adjust duration as needed
            size = size == Self.largeSize ? Self.smallSize : Self.largeSize
        }
    }
}

In order to match this CodePen animation: https://codepen.io/formfield/pen/zYeJzXz

However, I can't seem to actually get any animation to happen. Am I approaching this the right way?

1

There are 1 best solutions below

0
Sake On

I don't know if you've already found a solution, but I think the error is in how you animate the frame.

Window frames can be animated with window.setFrame(newFrame, display: true, animate: withAnimation) (https://developer.apple.com/documentation/appkit/nswindow/1419519-setframe)

The animation behaviour of this unfortunately cannot be adjusted with a NSAnimationContext.runAnimationGroup. To adjust the duration you can use: