You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于SwiftUI视图过渡的NSWindow动画无效果问题排查

macOS菜单栏应用窗口大小动画问题

我开发了一个macOS菜单栏应用,自定义了圆角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
    }
}

同时在AppDelegate中实现了调整窗口大小的函数:

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

        // 计算新的Y坐标,保持窗口顶部对齐
        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)
    }

我通过跟踪偏好设置窗口的显示状态,尝试据此更新并动画过渡窗口大小:
动画效果示例

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)) { // 按需调整时长
            size = size == Self.largeSize ? Self.smallSize : Self.largeSize
        }
    }
}

我的目标是实现平滑的窗口大小切换动画,但目前始终无法触发动画效果,请问我的实现思路是否正确?


问题分析与修复建议

  • 重复监听回调导致状态混乱
    代码中存在两个监听showingPreferences的onChange方法,两次修改size状态会导致逻辑冲突,直接删除其中一个即可。

  • 动画时长不匹配
    AppDelegate里的动画时长设置为10秒,而SwiftUI的withAnimation是1秒,两套动画系统时长不一致会导致同步问题,甚至看起来没有动画效果。将AppDelegate中的context.duration修改为1.0,保持和SwiftUI动画时长一致。

  • 双动画系统互相干扰
    同时使用SwiftUI的withAnimation修改状态和AppKit的NSWindow动画,两套动画系统会互相冲突。建议只保留一套逻辑:

    • 优先选择AppKit动画:去掉SwiftUI的withAnimation,直接在onChange中调用appDelegate.adjustWindowSize(to: newSize),因为窗口尺寸属于AppKit的NSWindow属性,用原生动画系统更可靠。
    • 如果要用SwiftUI驱动:需要将NSWindow的contentView大小绑定到SwiftUI的size状态,同时关闭NSWindow的自动调整功能,但这种方式对菜单栏应用窗口的适配性不如AppKit动画。
  • 窗口配置缺失
    确保RoundedWindow的styleMask包含.resizable,否则窗口无法调整大小;同时检查contentView的自动布局约束是否正确,避免约束冲突导致动画失效。

  • 冗余逻辑清理
    if viewModel.isVPNAvailable || !viewModel.isVPNAvailable这个判断永远成立,可直接简化为显示mainContentView,无需额外条件。


内容的提问来源于stack exchange,提问作者narner

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 05:40:58