Snapshot view is hidden while performing UIViewControllerAnimatedTransitioning - ios

I am trying to make a simple animated transitioning when pushing a UIViewController, but it seems I am missing something.
I animate a snapshot of a subview from the fromViewController to the toViewController. I am animating snapshot’s frame, but the snapshot is invisible for the whole duration of the animation.
Here is a simple code example. I am trying to animate a single UILabel from the first controller to the second. I specifically want to animate a snapshot taken from the toViewConroller and not from the fromViewController.
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let fromVC = transitionContext.viewController(forKey: .from) as! ViewController
let toVC = transitionContext.viewController(forKey: .to) as! SecondViewController
let container = transitionContext.containerView
toVC.view.frame = fromVC.view.frame
container.addSubview(toVC.view)
toVC.view.layoutIfNeeded()
let animatedFromView = fromVC.label!
let animatedToView = toVC.label!
let initialFrame = container.convert(animatedFromView.frame,
from: animatedFromView.superview)
let finalFame = container.convert(animatedToView.frame,
to: animatedToView.superview)
let snapshot = animatedToView.snapshotView(afterScreenUpdates: true)!
snapshot.frame = initialFrame
container.addSubview(snapshot)
animatedFromView.alpha = 0
animatedToView.alpha = 0
UIView.animate(withDuration: 2,
animations: {
snapshot.frame = finalFame
}) { (_) in
snapshot.removeFromSuperview()
fromVC.label.alpha = 1
toVC.label.alpha = 1
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
I guess that the snapshot is hidden, because setting the animatedToView’s alpha to 0, however I am not sure how to achieve that animation without setting that.

I tried your code its working fine.I changed a few things like initial frame hardcoded it so i can see the effect and also from viewController alpha.
::::::for presenting view controller
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let fromVC = transitionContext.viewController(forKey: .from) as! ViewController
let toVC = transitionContext.viewController(forKey: .to) as! SecondViewController
let container = transitionContext.containerView
toVC.view.frame = fromVC.view.frame
container.addSubview(toVC.view)
toVC.view.layoutIfNeeded()
let animatedFromView = fromVC.view!
let animatedToView = toVC.view!
let initialFrame = container.convert(CGRect(x: 0, y: 200, width: 100, height: 100),
from: animatedFromView.superview)
let finalFame = container.convert(animatedToView.frame,
to: animatedToView.superview)
let snapshot = animatedToView.snapshotView(afterScreenUpdates: true)!
snapshot.frame = initialFrame
container.addSubview(snapshot)
animatedFromView.alpha = 1
animatedToView.alpha = 0
UIView.animate(withDuration: 2,
animations: {
snapshot.frame = finalFame
}) { (_) in
snapshot.removeFromSuperview()
fromVC.view.alpha = 1
toVC.view.alpha = 1
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
:::::::::::::::::
Use while you are pushing view controller
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from) else { return }
guard let toView = transitionContext.view(forKey: UITransitionContextViewKey.to) else { return }
let container = transitionContext.containerView
container.insertSubview(toView, belowSubview: fromView)
let animatedFromView = fromView
let animatedToView = toView
let initialFrame = container.convert(CGRect(x: 0, y: 200, width: 100, height: 100),
from: animatedFromView.superview)
let finalFame = container.convert(animatedToView.frame,
to: animatedToView.superview)
let snapshot = animatedToView.snapshotView(afterScreenUpdates: true)!
snapshot.frame = initialFrame
container.addSubview(snapshot)
animatedFromView.alpha = 1
animatedToView.alpha = 1
UIView.animate(withDuration: 2,
animations: {
snapshot.frame = finalFame
}) { (_) in
snapshot.removeFromSuperview()
//fromVC.view.alpha = 1
//toVC.view.alpha = 1
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}

Related

iOS - Custom transition with Material design container transform effect

I am trying to achieve 3. Material design - container transform effect with custom transitioning in iOS. Below is the code for the presentation part.
class CustomTransition: NSObject {
var duration: TimeInterval = 5
}
extension CustomTransition:UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toViewController = transitionContext.viewController(forKey: .to),
let fromViewController = transitionContext.viewController(forKey: .from) as? ViewController else {
return
}
let finalFrame = transitionContext.finalFrame(for: toViewController)
toViewController.view.frame = fromViewController.menuButton.frame
toViewController.view.layer.cornerRadius = fromViewController.menuButton.frame.size.width / 2
toViewController.view.clipsToBounds = true
toViewController.view.alpha = 0
let archive = NSKeyedArchiver.archivedData(withRootObject: fromViewController.menuButton!)
let menuButtonCopy = NSKeyedUnarchiver.unarchiveObject(with: archive) as! UIButton
menuButtonCopy.layer.cornerRadius = menuButtonCopy.frame.size.width / 2
transitionContext.containerView.addSubview(menuButtonCopy)
transitionContext.containerView.addSubview(toViewController.view)
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
toViewController.view.frame = finalFrame
toViewController.view.alpha = 1
menuButtonCopy.alpha = 0
menuButtonCopy.frame = finalFrame
}, completion: { _ in
transitionContext.completeTransition(true)
menuButtonCopy.removeFromSuperview()
})
}
}
Here is the result
Actually, I want to align the '+' button in the centre of the container as long as it animates to the full screen as seen in the design. What is the I am missing here? Why is the '+' seen arising from bottom centre?

Transitioning Animation, UIViewControllerAnimatedTransitioning dismiss error?

Xcode 12.5.1 with Swift version 5.4.2
I use the following code to do Transitioning Animation for viewController dismissing
enum PresentingDirection{
case top, right, left, bottom
var bounds: CGRect{
UIScreen.main.bounds
}
func offsetF(withFrame viewFrame: CGRect) -> CGRect{
let h = bounds.size.height
let w = bounds.size.width
switch self {
case .top:
return viewFrame.offsetBy(dx: 0, dy: -h)
case .bottom:
return viewFrame.offsetBy(dx: 0, dy: h)
case .left:
return viewFrame.offsetBy(dx: -w, dy: 0)
case .right:
return viewFrame.offsetBy(dx: w, dy: 0)
}
}
}
class CustomDismissController: NSObject, UIViewControllerAnimatedTransitioning{
fileprivate var presentingDirection: PresentingDirection
init(direction orientation: PresentingDirection) {
presentingDirection = orientation
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 1
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromCtrl = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let toCtrl = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to), let toView = toCtrl.view else{
return
}
let finalCtrlFrame = transitionContext.finalFrame(for: fromCtrl)
let containerView = transitionContext.containerView
fromCtrl.view.alpha = 0.5
toView.frame = presentingDirection.offsetF(withFrame: finalCtrlFrame)
containerView.bringSubviewToFront(toView)
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: .curveLinear) {
toView.frame = finalCtrlFrame
} completion: { _ in
let success = !transitionContext.transitionWasCancelled
transitionContext.completeTransition(success)
}
}
}
The above is the the best I can do.
I want the effect fromCtrl.view.alpha = 1
As I tested, fromCtrl.view.alpha = 0.5 is very important.
Without it, the second view controller stays , I can not see the first view controller's frame animation
It seems like to be an iOS bug
Any good idea?
I also tried snapshotView
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromCtrl = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let toCtrl = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to), let toView = toCtrl.view, let snapshot = fromCtrl.view.snapshotView(afterScreenUpdates: false) else{
return
}
let finalCtrlFrame = transitionContext.finalFrame(for: fromCtrl)
let containerView = transitionContext.containerView
fromCtrl.view.isHidden = true
snapshot.frame = finalCtrlFrame
containerView.addSubview(snapshot)
toView.frame = presentingDirection.offsetF(withFrame: finalCtrlFrame)
containerView.bringSubviewToFront(toView)
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: .curveLinear) {
toView.frame = finalCtrlFrame
} completion: { _ in
let success = !transitionContext.transitionWasCancelled
transitionContext.completeTransition(success)
}
}
Not good.
full code in github
I solved.
The animation is the snapshot's thing.
Just layout to viewController's frame properly.
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromCtrl = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let toCtrl = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to), let toView = toCtrl.view, let snapshot = toView.snapshotView(afterScreenUpdates: true) else{
return
}
let finalCtrlFrame = transitionContext.finalFrame(for: fromCtrl)
let containerView = transitionContext.containerView
snapshot.frame =
presentingDirection.offsetF(withFrame: finalCtrlFrame)
containerView.addSubview(snapshot)
containerView.bringSubviewToFront(toView)
toView.frame = finalCtrlFrame
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: .curveLinear) {
snapshot.frame = finalCtrlFrame
} completion: { _ in
snapshot.removeFromSuperview()
let success = !transitionContext.transitionWasCancelled
transitionContext.completeTransition(success)
}
}
Failed to try to put snapshot under toView

How to make transition of a ViewController from Bottom to Top?

So here is a class for Slide in transition which adds a ViewController with animation from left to right and it works flawlessly I want a transition from bottom to top.
import UIKit
class SlideInTransition: NSObject, UIViewControllerAnimatedTransitioning {
var isPresenting = false
let dimmingView = UIView()
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toViewController = transitionContext.viewController(forKey: .to),
let fromViewController = transitionContext.viewController(forKey: .from) else { return }
let containerView = transitionContext.containerView
let finalWidth = toViewController.view.bounds.width * 0.8
let finalHeight = toViewController.view.bounds.height
if isPresenting {
// Add dimming view
dimmingView.backgroundColor = .black
dimmingView.alpha = 0.0
containerView.addSubview(dimmingView)
dimmingView.frame = containerView.bounds
// Add menu view controller to container
containerView.addSubview(toViewController.view)
// Init frame off the screen
toViewController.view.frame = CGRect(x: -finalWidth, y: 0, width: finalWidth, height: finalHeight)
}
// Move on screen
let transform = {
self.dimmingView.alpha = 0.5
toViewController.view.transform = CGAffineTransform(translationX: finalWidth, y: 0)
}
// Move back off screen
let identity = {
self.dimmingView.alpha = 0.0
fromViewController.view.transform = .identity
}
// Animation of the transition
let duration = transitionDuration(using: transitionContext)
let isCancelled = transitionContext.transitionWasCancelled
UIView.animate(withDuration: duration, animations: {
self.isPresenting ? transform() : identity()
}) { (_) in
transitionContext.completeTransition(!isCancelled)
}
}
}
To be honest I copied this code from somewhere a while ago and I don't have a source of it.
I'm fairly new to iOS so any help would be appreciated.
Try this,
class SlideInTransition: NSObject, UIViewControllerAnimatedTransitioning {
var isPresenting = false
let dimmingView = UIView()
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toViewController = transitionContext.viewController(forKey: .to),
let fromViewController = transitionContext.viewController(forKey: .from) else { return }
let containerView = transitionContext.containerView
let finalWidth = toViewController.view.bounds.width
let finalHeight = toViewController.view.bounds.height * 0.8
if isPresenting {
// Add dimming view
dimmingView.backgroundColor = .black
dimmingView.alpha = 0.0
containerView.addSubview(dimmingView)
dimmingView.frame = containerView.bounds
// Add menu view controller to container
containerView.addSubview(toViewController.view)
// Init frame off the screen
toViewController.view.frame = CGRect(x: 0, y: finalHeight, width: finalWidth, height: finalHeight)
}
// Move on screen
let transform = {
self.dimmingView.alpha = 0.5
toViewController.view.transform = CGAffineTransform(translationX: 0, y: -finalHeight)
}
// Move back off screen
let identity = {
self.dimmingView.alpha = 0.0
fromViewController.view.transform = .identity
}
// Animation of the transition
let duration = transitionDuration(using: transitionContext)
let isCancelled = transitionContext.transitionWasCancelled
UIView.animate(withDuration: duration, animations: {
self.isPresenting ? transform() : identity()
}) { (_) in
transitionContext.completeTransition(!isCancelled)
}
}
}

UIViewController interactive transition - Presented view disappears when interactive dismiss is canceled

I have a demo app where I'm trying to mimic the Mail app "new message" interactive transition - you can drag down to dismiss, but if you don't drag far enough the view pops back up and the transition is cancelled. I was able to duplicate the transition and interactivity in my demo app, but I noticed that when the dismiss transition is cancelled, the presented view controller is animated back up into place, then vanishes. Here's what it looks like:
My best guess is that the transition context's container view is being removed for some reason, since I added the presented view controller's view to it. Here is the presentation and dismiss code inside the UIViewControllerAnimatedTransitioning objects:
Show Transition
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to)
else {
return
}
let containerView = transitionContext.containerView
let finalFrame = transitionContext.finalFrame(for: toVC)
let duration = transitionDuration(using: transitionContext)
let topSafeAreaSpace = fromVC.view.safeAreaInsets.top // using fromVC safe area since it's on screen and has correct insets
let topGap: CGFloat = topSafeAreaSpace + 20
containerView.addSubview(toVC.view)
toVC.view.frame = CGRect(x: 0,
y: containerView.frame.height,
width: toVC.view.frame.width,
height: toVC.view.frame.height - 30)
UIView.animate(withDuration: duration, animations: {
toVC.view.frame = CGRect(x: finalFrame.minX,
y: finalFrame.minY + topGap,
width: finalFrame.width,
height: finalFrame.height - topGap)
let sideGap: CGFloat = 20
fromVC.view.frame = CGRect(x: sideGap,
y: topSafeAreaSpace,
width: fromVC.view.frame.width - 2 * sideGap,
height: fromVC.view.frame.height - 2 * topSafeAreaSpace)
fromVC.view.layer.cornerRadius = 10
fromVC.view.layoutIfNeeded()
}) { _ in
transitionContext.completeTransition(true)
}
}
Dismiss Transition
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to)
else {
return
}
let finalFrame = transitionContext.finalFrame(for: toVC)
let duration = transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration, animations: {
toVC.view.frame = finalFrame
fromVC.view.frame = CGRect(x: fromVC.view.frame.minX,
y: finalFrame.height,
width: fromVC.view.frame.width,
height: fromVC.view.frame.height)
toVC.view.layer.cornerRadius = 0
toVC.view.layoutIfNeeded()
}) { _ in
transitionContext.completeTransition(true)
}
}
And here is the code in the UIPercentDrivenInteractiveTransition object:
func handleGesture(_ gesture: UIPanGestureRecognizer) {
#warning("need to use superview?")
let translation = gesture.translation(in: gesture.view)
var progress = translation.y / 400
progress = min(1, max(0, progress)) // constraining value between 1 and 0
switch gesture.state {
case .began:
interactionInProgress = true
viewController.dismiss(animated: true, completion: nil)
case .changed:
shouldCompleteTransition = progress > 0.5
update(progress)
case .cancelled:
interactionInProgress = false
cancel()
case .ended:
interactionInProgress = false
if shouldCompleteTransition {
finish()
} else {
cancel()
}
default:
break
}
}
Any help would be greatly appreciated. It's worth noting I used this Ray Wenderlich tutorial as a reference -
https://www.raywenderlich.com/322-custom-uiviewcontroller-transitions-getting-started
However where they used image snapshots to animate the transition I'm using the view controller's views.
Thanks to a comment by #Dare, I realized all that was needed was a small update to the dismiss animation completion block:
// before - broken
transitionContext.completeTransition(true)
// after - WORKING!
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)

How access frame of a toView object in custom animation

I want to make an animation like iOS home screen folders, and I have to know the frame of the final position of the "folder":
I'm using a custom transition and here is the code of the animation file:
class FolderAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
let duration = 5.0
var presenting = true
func transitionDuration(_ transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(_ transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView()
let toViewC = transitionContext.viewController(forKey: UITransitionContextToViewControllerKey)! as! ProjectViewController
let fromViewC = transitionContext.viewController(forKey: UITransitionContextFromViewControllerKey)!as! ViewController
let cell : FolderCollectionViewCell = fromViewC.folderCollectionView.cellForItem(at: (fromViewC.folderCollectionView.indexPathsForSelectedItems()?.first!)!) as! FolderCollectionViewCell
let cellSnapshot: UIView = cell.snapshotView(afterScreenUpdates: false)!
let cellFrame = containerView.convert(cell.frame, from: cell.superview)
cellSnapshot.frame = cellFrame
cell.isHidden = true
toViewC.view.frame = transitionContext.finalFrame(for: toViewC)
toViewC.view.alpha = 0
containerView.addSubview(toViewC.view)
containerView.addSubview(cellSnapshot)
UIView.animate(withDuration: duration, animations: {
toViewC.view.alpha = 1.0
let finalFrame = containerView.convert(toViewC.containerView.frame, from: toViewC.view)
cellSnapshot.frame = finalFrame
}) { (_) in
cellSnapshot.removeFromSuperview()
transitionContext.completeTransition(true)
}
}
}
All works correctly, except let finalFrame = containerView.convert(toViewC.containerView.frame, from: toViewC.view) that set finalFrame values (is a CGRect variable) to (origin = (x = 0, y = 0), size = (width = 0, height = 0)).
I've followed this tutorial, writing my code in Swift
So, how can I access the property correctly?
The frame is the zero rect because at the point you are accessing it, the view's layout pass has not occurred. After you set the toVC.view frame, add this line:
toVC.view.layoutIfNeeded()
I've written about the use of snapshots in view controller transitions here if you're interested.

Resources