I\'m trying to create a simple transition animation between two view controllers, both of which have the same label. I simply want to animate the label from its position in the
The problem lies in dealing with coordinate systems. Consider these numbers:
fromViewController.label.frame: {{115.5, 313}, {144, 41}}
toViewController.titleLabel.frame: {{16, 12}, {144, 41}}
Those pairs of numbers are unrelated:
The frame of the label
is in the bounds coordinates of its superview, probably fromViewController.view
.
The frame of the titleLabel
is in the bounds coordinates of its superview, probably toViewController.view
.
Moreover, in most custom view transitions, the two view controller's views are in motion throughout the process. This makes it very difficult to say where the intermediate view should be at any moment in terms of either of them.
Thus you need to express the motion of this view in some common coordinate system, higher than either of those. That's why, in my answer here I use a snapshot view that's loose in the higher context view.
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:
frame
values to the coordinate space of the container and add all of these snapshots to the container view;alpha
at zero (so they fade in);alpha
back to 1
.alpha
to zero (so they fade out, which combined with point 4, yields a sort of cross dissolve).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:
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:
(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.