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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 23:10:27