3

我已经实现了一个导航控制器,以便结合旋转盘类型的布局(将每个VC布局成一个整体旋转的圆形视图)。控制器被配置为使用自定义的过渡类,如下图所示: -Swift 3中的自定义转换不能正确转换

 
import UIKit 

class RotaryTransition: NSObject, UIViewControllerAnimatedTransitioning { 
    let isPresenting :Bool 
    let duration :TimeInterval = 0.5 
    let animationDuration: TimeInterval = 0.7 
    let delay: TimeInterval = 0 
    let damping: CGFloat = 1.4 
    let spring: CGFloat = 6.0 

    init(isPresenting: Bool) { 
     self.isPresenting = isPresenting 
     super.init() 
    } 

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 
     //Get references to the view hierarchy 
     let fromViewController: UIViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)! 
     let toViewController: UIViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)! 
     let sourceRect: CGRect = transitionContext.initialFrame(for: fromViewController) 
     let containerView: UIView = transitionContext.containerView 

     if self.isPresenting { // Push 
      //1. Settings for the fromVC ............................ 
//   fromViewController.view.frame = sourceRect 
      fromViewController.view.layer.anchorPoint = CGPoint(x: 0.5, y: 3); 
      fromViewController.view.layer.position = CGPoint(x: fromViewController.view.frame.size.width/2, y: fromViewController.view.frame.size.height * 3); 

      //2. Setup toVC view........................... 
      containerView.insertSubview(toViewController.view, belowSubview:fromViewController.view) 
      toViewController.view.layer.anchorPoint = CGPoint(x: 0.5, y: 3); 
      toViewController.view.layer.position = CGPoint(x: toViewController.view.frame.size.width/2, y: toViewController.view.frame.size.height * 3); 
      toViewController.view.transform = CGAffineTransform(rotationAngle: 15 * CGFloat(M_PI/180)); 

      //3. Perform the animation............................... 
      UIView.animate(withDuration: animationDuration, delay:delay, usingSpringWithDamping: damping, initialSpringVelocity: spring, options: [], animations: { 
       fromViewController.view.transform = CGAffineTransform(rotationAngle: -15 * CGFloat(M_PI/180)); 
       toViewController.view.transform = CGAffineTransform(rotationAngle: 0); 
      }, completion: { 
       (animated: Bool) ->() in transitionContext.completeTransition(true) 
      }) 
     } else { // Pop 
      //1. Settings for the fromVC ............................ 
      fromViewController.view.frame = sourceRect 
      fromViewController.view.layer.anchorPoint = CGPoint(x: 0.5, y: 3); 
      fromViewController.view.layer.position = CGPoint(x: fromViewController.view.frame.size.width/2, y: fromViewController.view.frame.size.height * 3); 

      //2. Setup toVC view........................... 
//   toViewController.view.frame = transitionContext.finalFrame(for: toViewController) 
      toViewController.view.layer.anchorPoint = CGPoint(x: 0.5, y: 3); 
      toViewController.view.layer.position = CGPoint(x: toViewController.view.frame.size.width/2, y: toViewController.view.frame.size.height * 3); 
      toViewController.view.transform = CGAffineTransform(rotationAngle: -15 * CGFloat(M_PI/180)); 
      containerView.insertSubview(toViewController.view, belowSubview:fromViewController.view) 

      //3. Perform the animation............................... 
      UIView.animate(withDuration: animationDuration, delay:delay, usingSpringWithDamping: damping, initialSpringVelocity: spring, options: [], animations: { 
       fromViewController.view.transform = CGAffineTransform(rotationAngle: 15 * CGFloat(M_PI/180)); 
       toViewController.view.transform = CGAffineTransform(rotationAngle: 0); 
      }, completion: { 
       //When the animation is completed call completeTransition 
       (animated: Bool) ->() in transitionContext.completeTransition(true) 
      })    
     } 
    } 

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 
     return duration; 
    } 
} 

的意见如何运动的表示是显示在下面......两个红色区域是问题,后面会解释插图

enter image description here

呈现(推)翻译工作正常 - 2移动到1和3移动到2.然而,解雇(流行)翻译没有,从而解雇VC看起来正确地移动(2移动到3),但呈现(前一个)VC到达或者在错误的地方或者尺寸大小不正确的地方...

与类一样,翻译导致2移动到3(正确),但是1移动到4该视图的尺寸正确,但似乎偏离了预期位置的看似任意的距离。我从此尝试了各种不同的解决方案。

在弹出部我试图将下面的行(在代码注释): -

toViewController.view.frame = transitionContext.finalFrame(for: toViewController) 

...,但VC现在最终被收缩(1个移动至5)。我希望有人能看到我正在犯的可能的愚蠢错误。我试着简单地将push部分复制到pop部分(并反转所有内容),但它不起作用!

仅供参考......那些需要知道如何来装过渡到一个UINavigationController - 在UINavigationControllerDelegate添加到您的导航控制器,具有以下功能一起...

 
    func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { 
     let transition: SwingTransition = SwingTransition.init(isPresenting: (operation == .push ? true : false)) 
     return transition; 
    } 

下图显示了所有意见将共享相同的始发点(用于翻译)。目标是给出一个左轮手枪桶将每个VC移动到视野中的错觉。顶部中央视图表示查看窗口,显示堆栈中的第三个视图。道歉对穷人的视觉效果......

回答

3

的问题是,在恢复视图控制器的视图属性之一是没有得到正确复位。我建议在动画完成后重新设置它(如果您之后做了其他动画,假设视图未被转换,您可能不想保留非标准transformanchorPoint)。因此,在动画的completion块中,重置视图的position,anchorPointtransform

class RotaryTransition: NSObject, UIViewControllerAnimatedTransitioning { 
    let isPresenting: Bool 
    let duration: TimeInterval = 0.5 
    let delay: TimeInterval = 0 
    let damping: CGFloat = 1.4 
    let spring: CGFloat = 6 

    init(isPresenting: Bool) { 
     self.isPresenting = isPresenting 
     super.init() 
    } 

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 
     let from = transitionContext.viewController(forKey: .from)! 
     let to  = transitionContext.viewController(forKey: .to)! 
     let frame = transitionContext.initialFrame(for: from) 
     let height = frame.size.height 
     let width = frame.size.width 

     let angle: CGFloat = 15.0 * .pi/180.0 
     let rotationCenterOffset: CGFloat = width/2/tan(angle/2)/height + 1 // use fixed value, e.g. 3, if you want, or use this to ensure that the corners of the two views just touch, but don't overlap 

     let rightTransform = CATransform3DMakeRotation(angle, 0, 0, 1) 
     let leftTransform = CATransform3DMakeRotation(-angle, 0, 0, 1) 

     transitionContext.containerView.insertSubview(to.view, aboveSubview: from.view) 

     // save the anchor and position 

     let anchorPoint = from.view.layer.anchorPoint 
     let position = from.view.layer.position 

     // prepare `to` layer for rotation 

     to.view.layer.anchorPoint = CGPoint(x: 0.5, y: rotationCenterOffset) 
     to.view.layer.position = CGPoint(x: width/2, y: height * rotationCenterOffset) 
     to.view.layer.transform = self.isPresenting ? rightTransform : leftTransform 
     //to.view.layer.opacity = 0 

     // prepare `from` layer for rotation 

     from.view.layer.anchorPoint = CGPoint(x: 0.5, y: rotationCenterOffset) 
     from.view.layer.position = CGPoint(x: width/2, y: height * rotationCenterOffset) 

     // rotate 

     UIView.animate(withDuration: duration, delay: delay, usingSpringWithDamping: damping, initialSpringVelocity: spring, animations: { 
      from.view.layer.transform = self.isPresenting ? leftTransform : rightTransform 
      to.view.layer.transform = CATransform3DIdentity 
      //to.view.layer.opacity = 1 
      //from.view.layer.opacity = 0 
     }, completion: { finished in 
      // restore the layers to their default configuration 

      for view in [to.view, from.view] { 
       view?.layer.transform = CATransform3DIdentity 
       view?.layer.anchorPoint = anchorPoint 
       view?.layer.position = position 
       //view?.layer.opacity = 1 
      } 

      transitionContext.completeTransition(!transitionContext.transitionWasCancelled) 
     }) 
    } 

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 
     return duration 
    } 
} 

我做了一些其他杂物的变化,而我在这里:

  • 淘汰分号;
  • 消除了其中一个持续时间属性;
  • 修复completion关闭animate方法的参数名称到finished而不是animated以更准确地反映它的真实目的是...你也可以使用_;
  • 根据动画是否被取消设置completeTransition(因为如果你曾经做过这种互动/可取消,你不想总是使用true);
  • 使用.pi而不是M_PI;
  • 我评论了我对opacity的调整,但我通常会这样做,以使效果更加亮丽,并确保如果您调整角度以使视图重叠,则不会在其他视图中看到任何奇怪的人工制品当动画开始时或刚刚完成时;实际上,我已经计算了参数,因此不管屏幕尺寸如何,都没有重叠,因此没有必要,我已将opacity行注释掉,但可以考虑使用它们,具体取决于所需的效果。

此前我展示了如何简化这个过程,但最终的效果并不完全符合您的要求,但如果您有兴趣,请参阅previous rendition of this answer

+0

代码复习_and_问题解决,不错的工作! – jrturton

+0

这看起来像我期待的答案,它非常接近......不幸的是,过渡似乎来自不同的位置,破坏了视图固定在旋转光盘上的错觉。我放慢了动画速度,一些奇怪的原因,向左移动的视图来自视图右侧的某个位置,而右侧的视图来自左侧的某个位置。我试过在半宽处添加,但它们似乎没有效果 – NickSaintJohn

+0

想象一下,视图是左轮手枪中的子弹,而枪管就是视窗。他们应该从相同的翻译来源旋转。我仍然试图调整,但没有得到任何地方 – NickSaintJohn

3

您的问题是在执行自定义视图控制器转换时发生的常见问题。我知道这一点,因为我已经做了很多:)

你正在寻找流行转换中的问题,但实际的问题是推动。如果在转换后检查堆栈中第一个控制器的视图,则会看到它具有不寻常的框架,因为您已经对其转换和锚点以及图层位置等问题感到困惑。真的,你需要在结束转换之前清理所有这些,否则它会在以后的时候咬你,就像你在流行中看到的一样。

执行自定义转换的一种更简单更安全的方法是添加“画布”视图,然后在该画布上添加传出视图和传入视图的快照,然后对其进行处理。这意味着您在转换结束时没有清理 - 只需删除画布视图即可。我已经写了关于这种技术here。对于你的情况,我增加了以下简便方法:

extension UIView {  
    func snapshot(view: UIView, afterUpdates: Bool) -> UIView? { 
     guard let snapshot = view.snapshotView(afterScreenUpdates: afterUpdates) else { return nil } 
     self.addSubview(snapshot) 
     snapshot.frame = convert(view.bounds, from: view) 
     return snapshot 
    } 
} 

然后更新您的转换代码四处移动快照上的画布视图来代替:

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 
     //Get references to the view hierarchy 
     let fromViewController: UIViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)! 
     let toViewController: UIViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)! 
     let sourceRect: CGRect = transitionContext.initialFrame(for: fromViewController) 
     let containerView: UIView = transitionContext.containerView 

     // The canvas is used for all animation and discarded at the end 
     let canvas = UIView(frame: containerView.bounds) 
     containerView.addSubview(canvas) 

     let fromView = transitionContext.view(forKey: .from)! 
     let toView = transitionContext.view(forKey: .to)! 
     toView.frame = transitionContext.finalFrame(for: toViewController) 
     toView.layoutIfNeeded() 
     let toSnap = canvas.snapshot(view: toView, afterUpdates: true)! 

     if self.isPresenting { // Push 
      //1. Settings for the fromVC ............................ 
      //   fromViewController.view.frame = sourceRect 
      let fromSnap = canvas.snapshot(view: fromView, afterUpdates: false)! 
      fromView.removeFromSuperview() 
      fromSnap.layer.anchorPoint = CGPoint(x: 0.5, y: 3); 
      fromSnap.layer.position = CGPoint(x: fromViewController.view.frame.size.width/2, y: fromViewController.view.frame.size.height * 3); 

      //2. Setup toVC view........................... 
      toSnap.layer.anchorPoint = CGPoint(x: 0.5, y: 3); 
      toSnap.layer.position = CGPoint(x: toViewController.view.frame.size.width/2, y: toViewController.view.frame.size.height * 3); 
      toSnap.transform = CGAffineTransform(rotationAngle: 15 * CGFloat(M_PI/180)); 

      //3. Perform the animation............................... 
      UIView.animate(withDuration: animationDuration, delay:delay, usingSpringWithDamping: damping, initialSpringVelocity: spring, options: [], animations: { 
       fromSnap.transform = CGAffineTransform(rotationAngle: -15 * CGFloat(M_PI/180)); 
       toSnap.transform = CGAffineTransform(rotationAngle: 0); 
      }, completion: { 
       (animated: Bool) ->() in 
       containerView.insertSubview(toViewController.view, belowSubview:canvas) 
       canvas.removeFromSuperview() 
       transitionContext.completeTransition(true) 
      }) 
     } else { // Pop 
      //1. Settings for the fromVC ............................ 
      fromViewController.view.frame = sourceRect 
      fromViewController.view.layer.anchorPoint = CGPoint(x: 0.5, y: 3); 
      fromViewController.view.layer.position = CGPoint(x: fromViewController.view.frame.size.width/2, y: fromViewController.view.frame.size.height * 3); 

      //2. Setup toVC view........................... 
      let toSnap = canvas.snapshot(view: toView, afterUpdates: true)! 

      toSnap.layer.anchorPoint = CGPoint(x: 0.5, y: 3); 
      toSnap.layer.position = CGPoint(x: toViewController.view.frame.size.width/2, y: toViewController.view.frame.size.height * 3); 
      toSnap.transform = CGAffineTransform(rotationAngle: -15 * CGFloat(M_PI/180)); 

      //3. Perform the animation............................... 
      UIView.animate(withDuration: animationDuration, delay:delay, usingSpringWithDamping: damping, initialSpringVelocity: spring, options: [], animations: { 
       fromViewController.view.transform = CGAffineTransform(rotationAngle: 15 * CGFloat(M_PI/180)); 
       toSnap.transform = CGAffineTransform(rotationAngle: 0); 
      }, completion: { 
       //When the animation is completed call completeTransition 
       (animated: Bool) ->() in 
       containerView.insertSubview(toViewController.view, belowSubview: canvas) 
       canvas.removeFromSuperview() 
       transitionContext.completeTransition(true) 
      }) 
     } 
    } 

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 
     return duration; 
    } 
} 

这个特殊的过渡是非常简单的,所以它不是太难以重置视图框架的属性,但如果你做了更复杂的任何事情,那么canvas和快照方法运行得非常好,所以我倾向于在任何地方使用它。