如何在macOS实现类似iOS UIView.transition的NSView翻转动画?
实现macOS下类似iOS的UIView翻转动画效果
需求是在macOS应用的两个全屏NSView之间实现类似iOS中UIView.transition(from:self.upperView, to:self.lowerView,duration:1,options:[.transitionFlipFromRight])的翻转动画效果,但当前自定义动画未达预期。
原实现存在锚点偏移、透视空间错误、过渡生硬等问题,以下是修正后的完整实现:
1. 视图定义
private lazy var lowerView: NSView = { let view = NSView() view.wantsLayer = true view.layer?.backgroundColor = NSColor.red.cgColor view.translatesAutoresizingMaskIntoConstraints = false // 设置锚点为中心,确保旋转围绕视图中心进行 view.layer?.anchorPoint = CGPoint(x: 0.5, y: 0.5) return view }() private lazy var upperView: NSView = { let view = NSView() view.wantsLayer = true view.layer?.backgroundColor = NSColor.green.cgColor view.translatesAutoresizingMaskIntoConstraints = false view.layer?.anchorPoint = CGPoint(x: 0.5, y: 0.5) return view }()
2. 添加视图约束
private func setupViews() { self.view.addSubview(lowerView) self.view.addSubview(upperView) NSLayoutConstraint.activate([ // lowerView全屏约束 lowerView.topAnchor.constraint(equalTo: self.view.topAnchor), lowerView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor), lowerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), lowerView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor), // upperView全屏约束 upperView.topAnchor.constraint(equalTo: self.view.topAnchor), upperView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor), upperView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), upperView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor) ]) }
3. 修正后的FlipTransition动画类
import Cocoa public final class FlipTransition: NSObject { private var srcView, dstView: NSView? private var parentLayer: CALayer? private var duration: TimeInterval = 1 public func flip(from srcView: NSView, to dstView: NSView, duration: TimeInterval = 1) { self.duration = duration self.srcView = srcView self.dstView = dstView self.parentLayer = srcView.superview?.layer // 初始状态:显示源视图,隐藏目标视图 srcView.isHidden = false dstView.isHidden = true guard let parentLayer = parentLayer else { return } // 设置全局3D透视效果,应用到父视图层 var transform3D = CATransform3DIdentity transform3D.m34 = 1.0 / -1000.0 // 透视系数,值越小透视效果越强 parentLayer.sublayerTransform = transform3D // 第一阶段:源视图向右翻转至消失 animateView(srcView, from: CATransform3DIdentity, to: CATransform3DMakeRotation(-CGFloat.pi/2, 0, 1, 0)) { [weak self] finished in guard finished else { return } self?.startSecondPhase() } } private func startSecondPhase() { guard let srcView = srcView, let dstView = dstView else { return } // 切换视图可见性 srcView.isHidden = true dstView.isHidden = false // 第二阶段:目标视图从右侧翻转至显示 animateView(dstView, from: CATransform3DMakeRotation(CGFloat.pi/2, 0, 1, 0), to: CATransform3DIdentity) { [weak self] finished in guard finished else { return } self?.cleanup() } } private func cleanup() { guard let srcView = srcView, let dstView = dstView, let parentLayer = parentLayer else { return } // 清除动画并恢复初始状态 srcView.layer?.removeAllAnimations() dstView.layer?.removeAllAnimations() parentLayer.sublayerTransform = CATransform3DIdentity } private func animateView(_ view: NSView, from startTransform: CATransform3D, to endTransform: CATransform3D, completion: @escaping (Bool) -> Void) { let animation = CABasicAnimation(keyPath: "transform") animation.fromValue = startTransform animation.toValue = endTransform animation.duration = duration / 2 animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) animation.fillMode = .forwards animation.isRemovedOnCompletion = false // 使用CATransaction处理完成回调,避免代理循环引用 CATransaction.begin() CATransaction.setCompletionBlock { completion(true) } view.layer?.add(animation, forKey: "flipAnimation") CATransaction.commit() } }
4. 调用动画
override func viewDidAppear() { super.viewDidAppear() setupViews() DispatchQueue.main.asyncAfter(deadline: .now() + 2) { FlipTransition().flip(from: self.upperView, to: self.lowerView, duration: 1) } }
关键修改说明
- 锚点修正:将两个视图的layer锚点设置为
(0.5, 0.5),确保旋转围绕视图中心进行,避免偏移。 - 透视空间优化:把3D透视变换应用到父视图的layer,让两个子视图在统一的3D空间中动画,还原iOS转场的视觉效果。
- 回调逻辑简化:用
CATransaction的completionBlock替代CAAnimationDelegate,代码更简洁且规避循环引用风险。 - 状态恢复:动画结束后清除所有动画并重置父视图的3D变换,避免影响后续视图操作。
内容的提问来源于stack exchange,提问作者Jozef Porenut
相关产品推荐
相关产品推荐

