Sharing an Image Between Two Viewcontrollers During a Transition Animation

Sharing an Image between two viewControllers during a transition animation

It's probably two different views and an animated snapshot view. In fact, this is exactly why snapshot views were invented.

That's how I do it in my app. Watch the movement of the red rectangle as the presented view slides up and down:

Sample Image

It looks like the red view is leaving the first view controller and entering the second view controller, but it's just an illusion. If you have a custom transition animation, you can add extra views during the transition. So I create a snapshot view that looks just like the first view, hide the real first view, and move the snapshot view — and then remove the snapshot view and show the real second view.

Same thing here (such a good trick that I use it in a lot of apps): it looks like the title has come loose from the first view controller table view cell and slid up to into the second view controller, but it's just a snapshot view:

Sample Image

Display 2 view controllers at the same time with animation

So I have finished creating my answer, It takes a different approach than the other answers so bear with me.

Instead of adding a container view what I figured would be the best way was to create a UIViewController subclass (which I called CircleDisplayViewController). Then all your VCs that need to have this functionality could inherit from it (rather than from UIViewController).

This way all your logic for presenting and dismissing ResultViewController is handled in one place and can be used anywhere in your app.

The way your VCs can use it is like so:

class AnyViewController: CircleDisplayViewController { 

/* Only inherit from CircleDisplayViewController,
otherwise you inherit from UIViewController twice */

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}

@IBAction func showCircle(_ sender: UIButton) {

openCircle(withCenter: sender.center, radius: nil, resultDataSource: calculator!.iterateWPItems())

//I'll get to this stuff in just a minute

//Edit: from talking to Bright Future in chat I saw that resultViewController needs to be setup with calculator!.iterateWPItems()

}

}

Where showCircle will present your ResultViewController using the transitioning delegate with the circle center at the sending UIButtons center.

The CircleDisplayViewController subclass is this:

class CircleDisplayViewController: UIViewController, UIViewControllerTransitioningDelegate, ResultDelegate {

private enum CircleState {
case collapsed, visible
}

private var circleState: CircleState = .collapsed

private var resultViewController: ResultViewController!

private lazy var transition = CircularTransition()

func openCircle(withCenter center: CGPoint, radius: CGFloat?, resultDataSource: ([Items], Int, String)) {

let circleCollapsed = (circleState == .collapsed)

DispatchQueue.main.async { () -> Void in

if circleCollapsed {

self.addCircle(withCenter: center, radius: radius, resultDataSource: resultDataSource)

}

}

}

private func addCircle(withCenter circleCenter: CGPoint, radius: CGFloat?, resultDataSource: ([Items], Int, String])) {

var circleRadius: CGFloat!

if radius == nil {
circleRadius = view.frame.size.height/2.0
} else {
circleRadius = radius
}

//instantiate resultViewController here, and setup delegate etc.

resultViewController = UIStoryboard.resultViewController()

resultViewController.transitioningDelegate = self
resultViewController.delegate = self
resultViewController.modalPresentationStyle = .custom

//setup any values for resultViewController here

resultViewController.dataSource = resultDataSource

//then set the frame of resultViewController (while also setting endFrame)

let resultOrigin = CGPoint(x: 0.0, y: circleCenter.y - circleRadius)
let resultSize = CGSize(width: view.frame.size.width, height: (view.frame.size.height - circleCenter.y) + circleRadius)

resultViewController.view.frame = CGRect(origin: resultOrigin, size: resultSize)
resultViewController.endframe = CGRect(origin: resultOrigin, size: resultSize)

transition.circle = UIView()
transition.startingPoint = circleCenter
transition.radius = circleRadius

transition.circle.frame = circleFrame(radius: transition.radius, center: transition.startingPoint)

present(resultViewController, animated: true, completion: nil)

}

func collapseCircle() { //THIS IS THE RESULT DELEGATE FUNCTIONS

dismiss(animated: true) {

self.resultViewController = nil

}

}

func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {

transition.transitionMode = .dismiss
transition.circleColor = UIColor.red
return transition

}

func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {

transition.transitionMode = .present
transition.circleColor = UIColor.red
return transition

}

func circleFrame(radius: CGFloat, center: CGPoint) -> CGRect {
let circleOrigin = CGPoint(x: center.x - radius, y: center.y - radius)
let circleSize = CGSize(width: radius*2, height: radius*2)
return CGRect(origin: circleOrigin, size: circleSize)
}

}

public extension UIStoryboard {
class func mainStoryboard() -> UIStoryboard { return UIStoryboard(name: "Main", bundle: Bundle.main) }
}

private extension UIStoryboard {

class func resultViewController() -> ResultViewController {
return mainStoryboard().instantiateViewController(withIdentifier: "/* Your ID for ResultViewController */") as! ResultViewController
}

}

The only function that is called by the VCs that inherit from DisplayCircleViewController is openCircle, openCircle has a circleCenter argument (which should be your button center I'm guessing), an optional radius argument (if this is nil then a default value of half the view height is taken, and then whatever else you need to setup ResultViewController.

In the addCircle func there is some important stuff:

you setup ResultViewController however you have to before presenting (like you would in prepare for segue),

then setup the frame for it (I tried to make it the area of the circle that is visible but it is quite rough here, might be worth playing around with),

then this is where I reset the transition circle (rather than in the transition class), so that I could set the circle starting point, radius and frame here.

then just a normal present.

If you haven't set an identifier for ResultViewController you need to for this (see the UIStoryboard extensions)

I also changed the TransitioningDelegate functions so you don't set the circle center, this is because to keep it generic I put that responsibility to the ViewController that inherits from this one. (see top bit of code)

Finally I changed the CircularTransition class

I added a variable:

var radius: CGFloat = 0.0 //set in the addCircle function above

and changed animateTransition:

(removed the commented out lines):

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

let containerView = transitionContext.containerView

if transitionMode == .present {
if let presentedView = transitionContext.view(forKey: UITransitionContextViewKey.to) {

...

// circle = UIView()
// circle.frame = frameForCircle(withViewCenter: viewCenter, size: viewSize, startPoint: startingPoint)
circle.layer.cornerRadius = radius

...

}

} else {

if let returningView = transitionContext.view(forKey: UITransitionContextViewKey.from) {

...

// circle.frame = frameForCircle(withViewCenter: viewCenter, size: viewSize, startPoint: startingPoint)

...

}
}
}

Finally I made a protocol so that ResultViewController could dismiss the circle

protocol ResultDelegate: class {

func collapseCircle()

}

class ResultViewController: UIViewController {

weak var delegate: ResultDelegate!

var endFrame: CGRect!

var dataSource: ([Items], Int, String)! // same as in Bright Future's case

override func viewDidLoad() {
super.viewDidLoad()

}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}

override func viewDidLayoutSubviews() {
if endFrame != nil {
view.frame = endFrame
}
}

@IBAction func closeResult(_ sender: UIButton) {

delegate.collapseCircle()

}

}

This has turned out to be quite a huge answer, sorry about that, I wrote it in a bit a of rush so if anything is not clear just say.

Hope this helps!

Edit: I found the problem, iOS 10 has changed the way they layout views, so to fix this I added an endFrame property to ResultViewController and set it's views frame to that in viewDidLayoutSubviews. I also set both the frame and endFrame at the same time in addCircle. I changed the code above to reflect the changes. It's not ideal but I'll have another look later to see if there is a better fix.

Edit: this is what it looks like open for me

Sample Image

View Controller Transition animate subview position

You mention the animation of the subviews but you don't talk about the overall animation, but I'd be inclined to use the container view for the animation, to avoid any potential confusion/problems if you're animating the subview and the main view simultaneously. But I'd be inclined to:

  1. Make snapshots of where the subviews in the "from" view and then hide the subviews;
  2. Make snapshots of where the subviews in the "to" view and then hide the subviews;
  3. Convert all of these frame values to the coordinate space of the container and add all of these snapshots to the container view;
  4. Start the "to" snapshots' alpha at zero (so they fade in);
  5. Animate the changing of the "to" snapshots to their final destination changing their alpha back to 1.
  6. Simultaneously animate the "from" snapshots to the location of the "to" view final destination and animate their alpha to zero (so they fade out, which combined with point 4, yields a sort of cross dissolve).
  7. When all done, remove the snapshots and unhide the subviews whose snapshots were animated.

The net effect is a sliding of the label from one location to another, and if the initial and final content were different, yielding a cross dissolve while they're getting moved.

For example:

Sample Image

By using the container view for the animation of the snapshots, it's independent of any animation you might be doing of the main view of the destination scene. In this case I'm sliding it in from the right, but you can do whatever you want.

Or, you can do this with multiple subviews:

Sample Image

(Personally, if this were the case, where practically everything was sliding around, I'd lose the sliding animation of the main view because it's now becoming distracting, but it gives you the basic idea. Also, in my dismiss animation, I swapped around which view is being to another, which you'd never do, but I just wanted to illustrate the flexibility and the fading.)

To render the above, I used the following in Swift 4:

protocol CustomTransitionOriginator {
var fromAnimatedSubviews: [UIView] { get }
}

protocol CustomTransitionDestination {
var toAnimatedSubviews: [UIView] { get }
}

class Animator: NSObject, UIViewControllerAnimatedTransitioning {
enum TransitionType {
case present
case dismiss
}

let type: TransitionType

init(type: TransitionType) {
self.type = type
super.init()
}

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

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let fromVC = transitionContext.viewController(forKey: .from) as! CustomTransitionOriginator & UIViewController
let toVC = transitionContext.viewController(forKey: .to) as! CustomTransitionDestination & UIViewController

let container = transitionContext.containerView

// add the "to" view to the hierarchy

toVC.view.frame = fromVC.view.frame
if type == .present {
container.addSubview(toVC.view)
} else {
container.insertSubview(toVC.view, belowSubview: fromVC.view)
}
toVC.view.layoutIfNeeded()

// create snapshots of label being animated

let fromSnapshots = fromVC.fromAnimatedSubviews.map { subview -> UIView in
// create snapshot

let snapshot = subview.snapshotView(afterScreenUpdates: false)!

// we're putting it in container, so convert original frame into container's coordinate space

snapshot.frame = container.convert(subview.frame, from: subview.superview)

return snapshot
}

let toSnapshots = toVC.toAnimatedSubviews.map { subview -> UIView in
// create snapshot

let snapshot = subview.snapshotView(afterScreenUpdates: true)!// UIImageView(image: subview.snapshot())

// we're putting it in container, so convert original frame into container's coordinate space

snapshot.frame = container.convert(subview.frame, from: subview.superview)

return snapshot
}

// save the "to" and "from" frames

let frames = zip(fromSnapshots, toSnapshots).map { ($0.frame, $1.frame) }

// move the "to" snapshots to where where the "from" views were, but hide them for now

zip(toSnapshots, frames).forEach { snapshot, frame in
snapshot.frame = frame.0
snapshot.alpha = 0
container.addSubview(snapshot)
}

// add "from" snapshots, too, but hide the subviews that we just snapshotted
// associated labels so we only see animated snapshots; we'll unhide these
// original views when the animation is done.

fromSnapshots.forEach { container.addSubview($0) }
fromVC.fromAnimatedSubviews.forEach { $0.alpha = 0 }
toVC.toAnimatedSubviews.forEach { $0.alpha = 0 }

// I'm going to push the the main view from the right and dim the "from" view a bit,
// but you'll obviously do whatever you want for the main view, if anything

if type == .present {
toVC.view.transform = .init(translationX: toVC.view.frame.width, y: 0)
} else {
toVC.view.alpha = 0.5
}

// do the animation

UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
// animate the snapshots of the label

zip(toSnapshots, frames).forEach { snapshot, frame in
snapshot.frame = frame.1
snapshot.alpha = 1
}

zip(fromSnapshots, frames).forEach { snapshot, frame in
snapshot.frame = frame.1
snapshot.alpha = 0
}

// I'm now animating the "to" view into place, but you'd do whatever you want here

if self.type == .present {
toVC.view.transform = .identity
fromVC.view.alpha = 0.5
} else {
fromVC.view.transform = .init(translationX: fromVC.view.frame.width, y: 0)
toVC.view.alpha = 1
}
}, completion: { _ in
// get rid of snapshots and re-show the original labels

fromSnapshots.forEach { $0.removeFromSuperview() }
toSnapshots.forEach { $0.removeFromSuperview() }
fromVC.fromAnimatedSubviews.forEach { $0.alpha = 1 }
toVC.toAnimatedSubviews.forEach { $0.alpha = 1 }

// clean up "to" and "from" views as necessary, in my case, just restore "from" view's alpha

fromVC.view.alpha = 1
fromVC.view.transform = .identity

// complete the transition

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

// My `UIViewControllerTransitioningDelegate` will specify this presentation
// controller, which will clean out the "from" view from the hierarchy when
// the animation is done.

class PresentationController: UIPresentationController {
override var shouldRemovePresentersView: Bool { return true }
}

Then, to allow all of the above to work, if I'm transitioning from ViewController to SecondViewController, I'd specify what subviews I'm moving from and which ones I'm moving to:

extension ViewController: CustomTransitionOriginator {
var fromAnimatedSubviews: [UIView] { return [label] }
}

extension SecondViewController: CustomTransitionDestination {
var toAnimatedSubviews: [UIView] { return [label] }
}

And to support the dismiss, I'd add the converse protocol conformance:

extension ViewController: CustomTransitionDestination {
var toAnimatedSubviews: [UIView] { return [label] }
}

extension SecondViewController: CustomTransitionOriginator {
var fromAnimatedSubviews: [UIView] { return [label] }
}

Now, I don't want you to get lost in all of this code, so I'd suggest focusing on the high-level design (those first seven points I enumerated at the top). But hopefully this is enough for you to follow the basic idea.

How do I make an expand/contract transition between views on iOS?

  1. Making the effect is simple. You take the full-sized view, initialize its transform and center to position it on top of the thumbnail, add it to the appropriate superview, and then in an animation block reset the transform and center to position it in the final position. To dismiss the view, just do the opposite: in an animation block set transform and center to position it on top of the thumbnail, and then remove it completely in the completion block.

    Note that trying to zoom from a point (i.e. a rectangle with 0 width and 0 height) will screw things up. If you're wanting to do that, zoom from a rectangle with width/height something like 0.00001 instead.

  2. One way would be to do the same as in #1, and then call presentModalViewController:animated: with animated NO to present the actual view controller when the animation is complete (which, if done right, would result in no visible difference due to the presentModalViewController:animated: call). And dismissModalViewControllerAnimated: with NO followed by the same as in #1 to dismiss.

    Or you could manipulate the modal view controller's view directly as in #1, and accept that parentViewController, interfaceOrientation, and some other stuff just won't work right in the modal view controller since Apple doesn't support us creating our own container view controllers.

Combining an animation of an UIImageView with a transition

You can put your imageView inside a container, then animate the constraints of the container while doing the transition on the imageView.

iOS custom transition animation

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.

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 :

Sample Image

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

  • xcode : 9.2

  • swift : 4

Flip between two ViewControllers under the same NavigationController

You can add custom transition to the navigation controllers layer just before pushing the view controller.

let transition = CATransition()
transition.duration = 0.3
transition.type = "flip"
transition.subtype = kCATransitionFromLeft
self.navigationController?.view.layer.addAnimation(transition, forKey: kCATransition)
self.navigationController?.pushViewController(viewController!, animated: false)

Note that the animated parameter should be false. Otherwise the default sliding animation will takes place

Adding Image Transition Animation in Swift

class ViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!

let images = [
UIImage(named: "brooklyn-bridge.jpg")!,
UIImage(named: "grand-central-terminal.jpg")!,
UIImage(named: "new-york-city.jpg"),
UIImage(named: "one-world-trade-center.jpg")!,
UIImage(named: "rain.jpg")!,
UIImage(named: "wall-street.jpg")!]
var index = 0
let animationDuration: NSTimeInterval = 0.25
let switchingInterval: NSTimeInterval = 3

override func viewDidLoad() {
super.viewDidLoad()

imageView.image = images[index++]
animateImageView()
}

func animateImageView() {
CATransaction.begin()

CATransaction.setAnimationDuration(animationDuration)
CATransaction.setCompletionBlock {
let delay = dispatch_time(DISPATCH_TIME_NOW, Int64(self.switchingInterval * NSTimeInterval(NSEC_PER_SEC)))
dispatch_after(delay, dispatch_get_main_queue()) {
self.animateImageView()
}
}

let transition = CATransition()
transition.type = kCATransitionFade
/*
transition.type = kCATransitionPush
transition.subtype = kCATransitionFromRight
*/
imageView.layer.addAnimation(transition, forKey: kCATransition)
imageView.image = images[index]

CATransaction.commit()

index = index < images.count - 1 ? index + 1 : 0
}
}

Implement it as a custom image view would be better.



Related Topics



Leave a reply



Submit