基于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动画。
- 优先选择AppKit动画:去掉SwiftUI的
窗口配置缺失
确保RoundedWindow的styleMask包含.resizable,否则窗口无法调整大小;同时检查contentView的自动布局约束是否正确,避免约束冲突导致动画失效。冗余逻辑清理
if viewModel.isVPNAvailable || !viewModel.isVPNAvailable这个判断永远成立,可直接简化为显示mainContentView,无需额外条件。
内容的提问来源于stack exchange,提问作者narner
相关产品推荐
相关产品推荐

