I have a custom transition written in Swift where the dismissed view goes out as the presented view comes in from the side.
Now I want this same effect, but I want the presented view to come in from the top and the dismissed view go out at the bottom.
My code looks like this:
func animateTransition(transitionContext: UIViewControllerContextTransitioning){
let container = transitionContext.containerView()
let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)
let offScreenRight = CGAffineTransformMakeTranslation(container.frame.width, 0)
let offScreenLeft = CGAffineTransformMakeTranslation(-container.frame.width, 0)
if self.presenting == true{
toView?.transform = offScreenLeft
}else{
toView?.transform = offScreenRight
}
container.addSubview(toView!)
container.addSubview(fromView!)
let duration = self.transitionDuration(transitionContext)
UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.8, options: nil, animations: {
if self.presenting == true{
fromView?.transform = offScreenRight
}else{
fromView?.transform = offScreenLeft
}
toView?.transform = CGAffineTransformIdentity
}, completion: { finished in
// tell our transitionContext object that we've finished animating
transitionContext.completeTransition(true)
})
}
I thought this would be easy, as my logic tells the way to do this is to change from "width" to "height" in the toView, and fromView, but this does not work, and just creates the same effect as before, but it seems to skip one empty (black) view.
Any suggestions on how to achieve the desired effect would be appreciated.
The buggy code:
func animateTransition(transitionContext: UIViewControllerContextTransitioning){
let container = transitionContext.containerView()
let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)
let offScreenUp = CGAffineTransformMakeTranslation(container.frame.height, 0)
let offScreenDown = CGAffineTransformMakeTranslation(-container.frame.height, 0)
if self.presenting == true{
toView?.transform = offScreenDown
}else{
toView?.transform = offScreenUp
}
container.addSubview(toView!)
container.addSubview(fromView!)
let duration = self.transitionDuration(transitionContext)
UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.8, options: nil, animations: {
if self.presenting == true{
fromView?.transform = offScreenUp
}else{
fromView?.transform = offScreenDown
}
toView?.transform = CGAffineTransformIdentity
}, completion: { finished in
// tell our transitionContext object that we've finished animating
transitionContext.completeTransition(true)
})
}
You are still trying to translate in the X Coordinate. Try doing the translation in the Y coordinate.
let offScreenUp = CGAffineTransformMakeTranslation(0,container.frame.height)
let offScreenDown = CGAffineTransformMakeTranslation(0,-container.frame.height)
Related
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
I just upgraded to XCode9 with Swift4 today.
And I found that my UIViewControllerAnimatedTransitioning doesn't work as expected anymore.
The effect of this animation is that the fromView will scale down to 0.95 and the toView will slide in from the right side. The pop operation will do it reversely.
But now when I hit the back button of the NavigationBar, the start position of the toView isn't right. It displayed a original size toView and then scale up to 1.05.
Here's how I implement the transition animator.
// animate a change from one viewcontroller to another
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// get reference to our fromView, toView and the container view that we should perform the transition in
let container = transitionContext.containerView
let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from)!
let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)!
// set up from 2D transforms that we'll use in the animation
let offScreenRight = CGAffineTransform(translationX: container.frame.width, y: 0)
let offScreenDepth = CGAffineTransform(scaleX: 0.95, y: 0.95)
// start the toView to the right of the screen
if( presenting ){
toView.transform = offScreenRight
container.addSubview(fromView)
container.addSubview(toView)
}
else{
toView.transform = offScreenDepth
container.addSubview(toView)
container.addSubview(fromView)
}
// get the duration of the animation
// DON'T just type '0.5s' -- the reason why won't make sense until the next post
// but for now it's important to just follow this approach
let duration = self.transitionDuration(using: transitionContext)
// perform the animation!
// for this example, just slid both fromView and toView to the left at the same time
// meaning fromView is pushed off the screen and toView slides into view
// we also use the block animation usingSpringWithDamping for a little bounce
UIView.animate(withDuration: duration, delay: 0.0, options: .curveEaseOut, animations: {
if( self.presenting ){
fromView.transform = offScreenDepth
}
else{
fromView.transform = offScreenRight
}
toView.transform = .identity
}, completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
I didn't found anything special in this migration guide page.
https://swift.org/migration-guide-swift4/
What should I do to make the transition works again?
Before setting anything else, try to reset transformations of the fromView to identity:
// animate a change from one viewcontroller to another
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
...
UIView.animate(withDuration: duration, delay: 0.0, options: .curveEaseOut, animations: {
if( self.presenting ){
fromView.transform = offScreenDepth
}
else{
fromView.transform = offScreenRight
}
toView.transform = .identity
}, completion: { finished in
fromView.transform = .identity
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
....
}
I saw a tutorial on Appcoda Transition ViewControllers transition a menu from up to bottom and I implemented it. Then, I tried to transition from bottom up using UIViewControllerContextTransitioning. But, doing it wrong cause I was setting the wrong values I think. Below is the code
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
//Get reference to our fromView, toView and the container view
let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)
//Setup the transform for sliding
let container = transitionContext.containerView()
let height = container?.frame.height
let moveDown = CGAffineTransformMakeTranslation(0, height! - 150)
let moveUp = CGAffineTransformMakeTranslation(0, -50)
//Add both views to the container view
if isPresenting {
toView?.transform = moveUp
snapShot = fromView?.snapshotViewAfterScreenUpdates(true)
container?.addSubview(toView!)
container?.addSubview(snapShot!)
}
//Perform the animation
UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.3, options: UIViewAnimationOptions(rawValue: 0), animations: {
if self.isPresenting {
self.snapShot?.transform = moveDown
toView?.transform = CGAffineTransformIdentity
} else {
self.snapShot?.transform = CGAffineTransformIdentity
fromView?.transform = moveUp
}
}, completion: {finished in
transitionContext.completeTransition(true)
if !self.isPresenting {
self.snapShot?.removeFromSuperview()
}
})
}
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return duration
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
// Get reference to our fromView, toView and the container view
let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!
// Set up the transform we'll use in the animation
guard let container = transitionContext.containerView() else {
return
}
let moveUp = CGAffineTransformMakeTranslation(0, container.frame.height + 50)
let moveDown = CGAffineTransformMakeTranslation(0, -250)
// Add both views to the container view
if isPresenting {
toView.transform = moveUp
snapshot = fromView.snapshotViewAfterScreenUpdates(true)
container.addSubview(toView)
container.addSubview(snapshot!)
}
// Perform the animation
UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, options: [], animations: {
if self.isPresenting {
self.snapshot?.transform = moveDown
toView.transform = CGAffineTransformIdentity
} else {
self.snapshot?.transform = CGAffineTransformIdentity
fromView.transform = moveUp
}
}, completion: { finished in
transitionContext.completeTransition(true)
if !self.isPresenting {
self.snapshot?.removeFromSuperview()
}
})
}
This Should work. I checked out the tutorial you shared and you probably can't see the menu on the bottom because the way the MenuTableViewController.swift is set up on storyboard it is made so that the menu is always started from the top, so change that up and it should work perfectly fine. Let me know if you have any questions.
I'm presenting a view from another using a UIViewControllerTransitioningDelegate instance as transitioning delegate and modalPresentationStyle = Custom.
I'm using a TableViewController using static table view cells with UITextfields inside. Now when tapping over a text field whose borders are close to the the keyboard's frame, part of the tableView beneath the keyboard shows up. I also removed a UITapGestureRecognizer that I added to the semi-transparent background to make sure it's not part of the problem but the issue it's still there. Any ideas? Below is the animateTransition() method
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
dimmingView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
let containerView = transitionContext.containerView()
if let presentedViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) {
let presentedView = presentedViewController.view
let fromView = transitionContext.viewForKey(UITransitionContextFromViewControllerKey)
let centre = presentedView.center
if isPresenting {
presentedView.center = containerView.center
presentedView.frame = presentedView.bounds.rectByInsetting(dx: 30, dy: 150)
transitionContext.containerView().addSubview(presentedView)
dimmingView.frame = containerView.bounds
dimmingView.alpha = 0.0
containerView.insertSubview(dimmingView, atIndex: 0)
presentedViewController.transitionCoordinator()?.animateAlongsideTransition({
context in
self.dimmingView.alpha = 1.0
}, completion: nil)
}
else {
presentedViewController.transitionCoordinator()?.animateAlongsideTransition({
context in
self.dimmingView.alpha = 0.0
}, completion: {
context in
self.dimmingView.removeFromSuperview()
})
}
UIView.animateWithDuration(self.transitionDuration(transitionContext),
delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 10.0, options: nil,
animations: {
presentedView.center = centre
}, completion: {
_ in
transitionContext.completeTransition(true)
})
}
}
The effect could be implemented like the following code in animateTransition method:
UIView.animateWithDuration(duration,
delay: 0,
usingSpringWithDamping: 0.3,
initialSpringVelocity: 0.0,
options: .CurveLinear,
animations: {
fromVC.view.alpha = 0.5
toVC.view.frame = finalFrame
},
completion: {_ -> () in
fromVC.view.alpha = 1.0
transitionContext.completeTransition(true)
})
But how could I implement it using gravity and collision behaviors(UIGravityBehavior, UICollisionBehavior)?
And a more general question may be "How to use the UIDynamicAnimator to customize the transitions between UIViewControllers?"
You can find the solution under the post Custom view controller transitions with UIDynamic behaviors by dasdom.
And the Swift code:
func transitionDuration(transitionContext: UIViewControllerContextTransitioning!) -> NSTimeInterval {
return 1.0
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning!) {
// 1. Prepare for the required components
let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
let finalFrame = transitionContext.finalFrameForViewController(toVC)
let containerView = transitionContext.containerView()
let screenBounds = UIScreen.mainScreen().bounds
// 2. Make toVC at the top of the screen
toVC.view.frame = CGRectOffset(finalFrame, 0, -1.0 * CGRectGetHeight(screenBounds))
containerView.addSubview(toVC.view)
// 3. Set the dynamic animators used by the view controller presentation
var animator: UIDynamicAnimator? = UIDynamicAnimator(referenceView: containerView)
let gravity = UIGravityBehavior(items: [toVC.view])
gravity.magnitude = 10
let collision = UICollisionBehavior(items: [toVC.view])
collision.addBoundaryWithIdentifier("GravityBoundary",
fromPoint: CGPoint(x: 0, y: screenBounds.height),
toPoint: CGPoint(x: screenBounds.width, y: screenBounds.height))
let animatorItem = UIDynamicItemBehavior(items: [toVC.view])
animatorItem.elasticity = 0.5
animator!.addBehavior(gravity)
animator!.addBehavior(collision)
animator!.addBehavior(animatorItem)
// 4. Complete the transition after the time of the duration
let nsecs = transitionDuration(transitionContext) * Double(NSEC_PER_SEC)
let delay = dispatch_time(DISPATCH_TIME_NOW, Int64(nsecs))
dispatch_after(delay, dispatch_get_main_queue()) {
animator = nil
transitionContext.completeTransition(true)
}
}
A little more complicated than using animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion: method.
EDIT: Fixed a bug when 'transitionDuration' is ≤1