Jack Daniel
Jack Daniel

Reputation: 2467

Custom UIViewController animation issue

I created a simple custom transition animation for UIViewController with UIViewControllerAnimatedTransitioning. Everything is working, but when target view presents, content starts appear from bottom right corner and when view is disappear, content disappear from left top corner. I attached gif with example. This is my code:

CustomViewAnimator.swift

class CustomViewAnimator: NSObject, UIViewControllerAnimatedTransitioning {

    private let interval = 0.5
    var forward = false
    var originFrame: CGRect = .zero

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

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

        let containerView = transitionContext.containerView

        guard let toView = transitionContext.view(forKey: .to) else {return}
        guard let fromView = transitionContext.view(forKey: .from) else {return}
        let destinationView = forward ? toView : fromView
        destinationView.alpha = forward ? 0 : 1

        forward ? containerView.addSubview(toView) : containerView.insertSubview(toView, belowSubview: fromView)
        forward ? (destinationView.frame = originFrame) : (destinationView.frame = fromView.frame)

        UIView.animate(withDuration: interval, animations: {

            self.forward ? (destinationView.frame = fromView.frame) : (destinationView.frame = self.originFrame)
            destinationView.alpha = self.forward ? 1 : 0

        }) { (finished) in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        }

    }

}

MainViewController.self

class MainViewController: UIViewController {

    private var button: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()
        setup()
    }

    @objc private func openView() {
        let viewController = TargetViewController()
        viewController.transitioningDelegate = self
        navigationController?.pushViewController(viewController, animated: true)

    }
}

extension MainViewController: UINavigationControllerDelegate {
    func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {

        let animator = CustomViewAnimator()
        animator.originFrame = button.frame
        animator.forward = (operation == .push)
        return animator
    }
}

extension MainViewController: UIViewControllerTransitioningDelegate {
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        let animator = CustomViewAnimator()
        animator.forward = true
        return animator
    }

    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        let animator = CustomViewAnimator()
        animator.forward = false
        return animator
    }
}

TargetViewController.swift

class TargetViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        setup()
    }

}

extension TargetViewController {

    fileprivate func setup() {
        view.backgroundColor = .orange
        navigationItem.title = "Target view"

        let label: UILabel = {
            let label = UILabel()
            label.translatesAutoresizingMaskIntoConstraints = false
            label.text = "Hello from target view :)"
            label.textColor = .white
            label.font = UIFont.boldSystemFont(ofSize: 18)
            return label
        }()
        view.addSubview(label)

        label.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        label.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
    }

}

Can anybody say how I can fix this issue, so label(or any other content) in TargetViewController stayed fixed on their places?

enter image description here

Upvotes: 3

Views: 199

Answers (2)

E.Coms
E.Coms

Reputation: 11529

I agree with the discussion from other answers. My suggestion is instead of animating the frame of UIView, you may animate the mask of the destination View as following. It should be the effect of what the oper really wants.

The bold part is the difference from the original post.

 func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

    let containerView = transitionContext.containerView

    guard let toView = transitionContext.view(forKey: .to) else {return}
    guard let fromView = transitionContext.view(forKey: .from) else {return}
    let destinationView = forward ? toView : fromView
    destinationView.alpha = forward ? 0 : 1

let maskView = UIView.init(frame: forward ? originFrame : fromView.frame)

maskView.backgroundColor = UIColor.white

destinationView.mask = maskView

    forward ? containerView.addSubview(toView) : containerView.insertSubview(toView, belowSubview: fromView)
    forward ? (destinationView.frame = toView.frame) : (destinationView.frame = fromView.frame)


    UIView.animate(withDuration: interval, animations: {

self.forward ? (destinationView.mask?.frame = fromView.frame) : (destinationView.mask?.frame = self.originFrame)

        destinationView.alpha = self.forward ? 1 : 0
            }) { (finished) in

        transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
    }

}

Upvotes: 1

Peter Tretiakov
Peter Tretiakov

Reputation: 3410

Basically your problem is because of mixing Auto Layout and frames. You position subviews (button and label) in ViewControllers' views with constraints but for animation you use frames updates. So as I understand correctly how layout works your label constraints are not active during transition or at least are not used by layout engine to calculate position of label.

So, the best advice is to avoid frame animation in transition and try to create appropriate constraints for .from and .to views and animate constraints' constants. It would take more code, but you can be sure that you don't mix two different strategies for layout.

Easier solution is to tell containerView to calculate constraints and to position elements appropriately during animation by simply add containerView.layoutIfNeeded() to animations block. So it would be:

UIView.animate(withDuration: interval, animations: {
    self.forward ? (destinationView.frame = fromView.frame) : (destinationView.frame = self.originFrame)
    destinationView.alpha = self.forward ? 1 : 0
    containerView.layoutIfNeeded()
}) { (finished) in
    transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}

But that only makes things work and doesn't solve the main problem with simultaneous usage of two layout strategies.

Upvotes: 1

Related Questions