SwiftUI内容动态变更时如何实现NSWindow窗口大小的平滑动画

解决方案
出现该问题的核心原因是SwiftUI的内容动画默认仅在SwiftUI渲染层执行,NSWindow的自动尺寸适配逻辑没有关联SwiftUI的动画事务,只会捕获内容最终尺寸直接跳转。
方案1:全场景通用适配(支持SwiftUI生命周期)
通过NSViewRepresentable桥接获取当前视图所在的NSWindow,绑定SwiftUI动画事务同步触发窗口尺寸动画,无需手动管理Window实例。
首先添加工具扩展:
import AppKit import SwiftUI extension NSView { /// 获取当前NSView所属的NSWindow var hostWindow: NSWindow? { var next = nextResponder while let responder = next { if let window = responder as? NSWindow { return window } next = responder.nextResponder } return nil } } extension View { /// 绑定当前视图的动画事务,同步触发所属NSWindow的尺寸动画 func syncWindowSizeAnimation() -> some View { background(WindowAnimationSyncView()) } } private struct WindowAnimationSyncView: NSViewRepresentable { func makeNSView(context: Context) -> NSView { NSView(frame: .zero) } func updateNSView(_ nsView: NSView, context: Context) { guard let window = nsView.hostWindow, let animation = context.transaction.animation else { return } // 同步SwiftUI动画参数到AppKit动画上下文 NSAnimationContext.runAnimationGroup { ctx in ctx.duration = animation.duration ctx.timingFunction = animation.timingFunction as? CAMediaTimingFunction ctx.allowsImplicitAnimation = true // 用animator对象触发窗口尺寸渐变 window.animator().setContentSize(window.contentView?.fittingSize ?? .zero) } } }
使用方式非常简单,只需在你的根视图上添加syncWindowSizeAnimation()修饰符即可:
struct ContentView: View { @State var isExpanded = false var body: some View { VStack { Button(isExpanded ? "Close" : "Open", action: toggleExpand) if isExpanded { ForEach(0..<5) { count in Text("\(count). Some Item") } } } .padding() .fixedSize() .syncWindowSizeAnimation() // 仅需添加这一行 } func toggleExpand() { withAnimation(.easeInOut(duration: 1)) { isExpanded.toggle() } } }
方案2:手动创建NSWindow场景适配
如果你是通过传统AppKit方式手动创建NSWindow,直接配置Window属性并监听尺寸变更即可:
// 创建Window时配置动画属性 let window = NSWindow(contentRect: .zero, styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: false) window.animationBehavior = .documentWindow window.contentView?.postsFrameChangedNotifications = true // 监听内容视图尺寸变更 NotificationCenter.default.addObserver(forName: NSView.frameDidChangeNotification, object: window.contentView, queue: .main) { _ in NSAnimationContext.runAnimationGroup { ctx in ctx.duration = 1 ctx.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) window.animator().setContentSize(window.contentView?.fittingSize ?? .zero) } }
注意事项
- 提前配置窗口的
minSize和maxSize,确保动画过程中的中间尺寸不会超出限制,避免动画卡顿 - 不要在用户手动拖拽窗口边缘调整尺寸的同时触发该动画,避免事件冲突
- 该方案兼容macOS 12及以上系统,更低版本可将
context.transaction相关逻辑替换为手动传入动画参数即可
内容的提问来源于stack exchange,提问作者Martin Powlette Jr
相关产品推荐
相关产品推荐

