iOS custom transition animation
Asked Answered
S

2

5

I have started learning of custom transition animation with using UIViewControllerAnimatedTransitioning protocol. And mostly all videos I've found on youtube are based on the flow when we have new ViewController presented with circle animation or similar to it.

I have problems with implementation my way of transitions. Mostly, what I need, is similar to the facebook app and how they open full-screen image viewer.

So, lets say we have VC1 and VC2. On VC1 we call action to present VC2. And on both VCs we have the same UI element. In my case that is UIImageView. Like you click on imageView on VC1 and it opens detail page for some object with its image at the top. And I want to have animation, that should look like image from VC1 is changing frame to the final frame of image from VC2, and then other content (like labels, buttons, etc) on detail page should appear.

But I've faced with some problems during training.
1. First of all, I don't understand the idea of containerView of transitionContext. But as I see, it is something like a middle-state view between between transitions. Is that correct? But that works strange to me, since even backgroundColor property not working for containerView.
2. I don't understand what exactly I need to animate during transition, and what should be the structure of the containerView subViews. In my example, when presenting VC2, I need, as I understand, to kinda hide all its subViews. Then animate imageView from VC1 to the frame of imageView from VC2, and then make visible all subViews again. So, in this case imageView should be added to containerView? If so, then should it be the actual imageView from VC1, or that is fully new copy of imageView, with the same frame/image, that is just temporarily used during transitions...

It will be helpful to link me to examples/tutorial/code with similar animation

Here is link to how that works in facebook

Salvatoresalvay answered 23/5, 2017 at 12:19 Comment(0)
A
12

Understanding custom transition animation

Like if you'r navigating from VCA to VCB then

  1. First of all you need to use the UIViewControllerTransitioningDelegate.

The transitioning delegate is responsible for providing the animation controller to be used for the custom transition. The delegate object you designate must conform to the UIViewControllerTransitioningDelegate protocol.

  1. Now you have to use UIViewControllerAnimatedTransitioning

It is responsible for the transition in terms of both the duration and the actual logic of animating the views.

These delegates work like you are in between two VC's and playing with them.

To make the complete transition as successful you have to do below steps:

  1. So for using it first of all you need to

    • set modalPresentationStyle = .custom
    • assign transitonDelegate property.
  2. In func animateTransition(_ : ) you have to use context containerView because you'r in between two VC's so you need any container where you can do any animation, so context provides you that container where you can do animation.

  3. Now you need fromView & toView i.e. VCA.view & VCB.view resp. Now add these two views in containerView and write core logic of animation.

  4. The last important thing to note is the completeTransition(_:) method called on the transition context object. This method must be called once your animation has completed to let the system know that your view controllers have finished transitioning.

This is core fundamental of transition animation.

I don't know FB animation so I just explained rest of your question.

Reference

Any further info you can ask.

Code Addition

On image selection

add in VC_A

var selectedImage: UIImageView?
 let transition = PopAnimator()

  override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)

        coordinator.animate(
          alongsideTransition: {context in
            self.bgImage.alpha = (size.width>size.height) ? 0.25 : 0.55
            self.positionListItems()
          },
          completion: nil
        )
      }
//position all images inside the list
  func positionListItems() {
    let listHeight = listView.frame.height
    let itemHeight: CGFloat = listHeight * 1.33
    let aspectRatio = UIScreen.main.bounds.height / UIScreen.main.bounds.width
    let itemWidth: CGFloat = itemHeight / aspectRatio

    let horizontalPadding: CGFloat = 10.0

    for i in herbs.indices {
      let imageView = listView.viewWithTag(i) as! UIImageView
      imageView.frame = CGRect(
        x: CGFloat(i) * itemWidth + CGFloat(i+1) * horizontalPadding, y: 0.0,
        width: itemWidth, height: itemHeight)
    }

    listView.contentSize = CGSize(
      width: CGFloat(herbs.count) * (itemWidth + horizontalPadding) + horizontalPadding,
      height:  0)
  }

// On image selection
VC_B.transitioningDelegate = self
    present(VC_B, animated: true, completion: nil)



   // add extension
extension VC_A: UIViewControllerTransitioningDelegate {

  func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    transition.originFrame = selectedImage!.superview!.convert(selectedImage!.frame, to: nil)

    transition.presenting = true
    selectedImage!.isHidden = true

    return transition
  }

  func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    transition.presenting = false
    return transition
  }
}

and animation class

class PopAnimator: NSObject, UIViewControllerAnimatedTransitioning {

  let duration = 1.0
  var presenting = true
  var originFrame = CGRect.zero

  var dismissCompletion: (()->Void)?

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

  func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    let containerView = transitionContext.containerView

    let toView = transitionContext.view(forKey: .to)!

    let herbView = presenting ? toView : transitionContext.view(forKey: .from)!

    let initialFrame = presenting ? originFrame : herbView.frame
    let finalFrame = presenting ? herbView.frame : originFrame

    let xScaleFactor = presenting ?

      initialFrame.width / finalFrame.width :
      finalFrame.width / initialFrame.width

    let yScaleFactor = presenting ?

      initialFrame.height / finalFrame.height :
      finalFrame.height / initialFrame.height

    let scaleTransform = CGAffineTransform(scaleX: xScaleFactor, y: yScaleFactor)

    if presenting {
      herbView.transform = scaleTransform
      herbView.center = CGPoint(
        x: initialFrame.midX,
        y: initialFrame.midY)
      herbView.clipsToBounds = true
    }

    containerView.addSubview(toView)
    containerView.bringSubview(toFront: herbView)

    UIView.animate(withDuration: duration, delay:0.0, usingSpringWithDamping: 0.4,
      initialSpringVelocity: 0.0,
      animations: {
        herbView.transform = self.presenting ?
          CGAffineTransform.identity : scaleTransform
        herbView.center = CGPoint(x: finalFrame.midX,
                                  y: finalFrame.midY)
      },
      completion:{_ in
        if !self.presenting {
          self.dismissCompletion?()
        }
        transitionContext.completeTransition(true)
      }
    )
  }

}

Output :

enter image description here

Git-hub Repo: https://github.com/thedahiyaboy/TDCustomTransitions

  • xcode : 9.2

  • swift : 4

Adjunction answered 23/5, 2017 at 12:44 Comment(6)
Hello. Thanks for response. I've added a link to how that looks in facebook. Maybe it will help you to understand what I'm trying to achieve. I don't care about "magnet" effect, and stuff like that. I just want to have the same animation, so it looks like the imageView from full-screen gallery is the same UIImageView from the listSalvatoresalvay
thanks. So, as I understood the idea is to add (in case of presenting action) toView to the containerView immediately with some UI changes, then animate it to the final state, and complete transition. Right?Salvatoresalvay
If so, then I have a question about best practices... What is the best way to access, change view to its initial state, and animate it to the final state? For example, I have VC_B with a lot of fields/subviews, not just full-screen imageView, but I want an animation that will just show image (scale) initially, and then show all other fields. This requires to kinda iterate through all objects, hide them before the actual animation, animate image to its final frame, and then iterate through all objects again to show them. Right?Salvatoresalvay
As i understand, First you wanted to show image on VC_B. When image in shown then you wanted to show some other components with animation (If i am wrong then send link of your description). For doing this, first use animateTransition to animate the the VC. Then in ViewDidAppear of VC_B animate your other components as per your logic. NOTE: transitionAnimate is used to animate the VC's not for the components.Adjunction
If we are not doing modal presenting insteed if we are adding view controller like addChildViewController addsubview, will transition animation will apply ?Gobbledegook
@IraniyaNaynesh transition animation is only used when you presenting any VC and push your VC. But in your case, you adding a childVC view as a subview. Here, there is no transition is occurring so it does not work. To make transition you have use simple UIview animation after adding the childView as a subView. Hope now you are cleared.Adjunction
P
2

The core method of UIViewControllerAnimatedTransitioning is animateTransition. I have added comments here while trying to explain the basic idea.

let duration = 0.5
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    // Works like an empty scratchpad/slate.
    // This is the view that will be shown on screen when animation starts and upto 
    // it ends.
    // Any animations done here are visible to user.
    // Nothing right now, in this container(*).
    let containerView = transitionContext.containerView

    // Grab the controller to animate from and to.
    let fromView = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!.view
    let toView = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!.view

    // We might need some other view's that need to be animated, like the UIImageView
    // In  your case this image view must exists on fromView and also in toView
    let fromImageView = UIImageView() // We should get image from fromController
    let toImageView = UIImageView() // We should get image from toController

    // Since the containerView has no views as of now(*), we need to add our fromView first
    containerView.addSubview(fromView!)

    // We will also add, the to view but with alpha 0 so that is not visible initially
    toView?.alpha = 0.0
    // Add this to view to container
    containerView.addSubview(toView!)


    UIView.animate(withDuration: duration, animations: { 
        // We do animations here, something like,
        fromImageView.frame = (toView?.frame)! // With some checking around the view relative frames
        toView?.alpha = 1.0
    }) { (completed) in
        // Do clean up here, after this completeTransition(true) method,
        // the comtainer will be removed from the screen and toView will be shown automatically
        transitionContext.completeTransition(true)
    }
}
Plunder answered 23/5, 2017 at 13:5 Comment(4)
Hello. Thanks for response. The part with "ImageView" in the middle of the method, is actually what I was wondering. But the problem is that with dismissing animation I get nil when accessing viewController(forKey:). So I will not be able to create opposite animation when on dismissSalvatoresalvay
@StasIvanov is nil for fromviewcontroller key?Plunder
Yes, once I was trying to create custom transition for dismiss, I got viewController(forKey: .from) nil, while viewForKey... worked fine. But I will check again...Salvatoresalvay
Oops, my bad. Sorry :) Works as expectedSalvatoresalvay

© 2022 - 2024 — McMap. All rights reserved.