I have created a Viewcontrollertransition and everything is working as long as I don't change the device orientation.
Image 1 shows the screen as it should be. I then switch to the next viewcontroller where I change the orientation. Now I go back to first viewcontroller and again switch the orientation. Then I get the result visible in image 2. A black border appears. Please don't mind the white box in the center of the screen.
Below you find the code of my animation. Can you see what is wrong?
import Foundation
import UIKit
class ZoomOutCircleViewTransition: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
var hideDelayed = false
var transitionContext: UIViewControllerContextTransitioning?
init(hideDelayed : Bool = true) {
self.hideDelayed = hideDelayed
super.init()
}
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.6
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
self.transitionContext = transitionContext
guard let toViewController: UIViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) else {
return
}
guard let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) else {
return
}
guard let toViewTransitionFromView = toViewController as? TransitionFromViewProtocol else {
return
}
let containerView = transitionContext.containerView()
let imageViewSnapshot = toViewTransitionFromView.getViewForTransition()
imageViewSnapshot.alpha = 0.0
let imageViewSnapshotOriginalFrame = imageViewSnapshot.frame
let startFrame = CGRectMake(-CGRectGetWidth(toViewController.view.frame)/2, -CGRectGetHeight(toViewController.view.frame)/2, CGRectGetWidth(toViewController.view.frame)*2, CGRectGetHeight(toViewController.view.frame)*2)
let quadraticStartFrame = CGRect(x: startFrame.origin.x - (startFrame.height - startFrame.width)/2, y: startFrame.origin.y, width: startFrame.height, height: startFrame.height)
containerView!.insertSubview(toViewController.view, atIndex: 0)
containerView!.addSubview(imageViewSnapshot)
// UIViewController circle shrink animation
let bigCirclePath = UIBezierPath(ovalInRect: quadraticStartFrame)
let smallCirclePath = UIBezierPath(ovalInRect: imageViewSnapshot.frame)
let maskLayer = CAShapeLayer()
maskLayer.frame = toViewController.view.frame
maskLayer.path = bigCirclePath.CGPath//maskPath.CGPath
fromViewController.view.layer.mask = maskLayer
let pathAnimation = CABasicAnimation(keyPath: "path")
pathAnimation.delegate = self
pathAnimation.fromValue = bigCirclePath.CGPath
pathAnimation.toValue = smallCirclePath.CGPath
pathAnimation.duration = transitionDuration(transitionContext)
maskLayer.path = smallCirclePath.CGPath
maskLayer.addAnimation(pathAnimation, forKey: "pathAnimation")
// pathAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
imageViewSnapshot.frame = quadraticStartFrame
// Make imageView visible with animation
let showImageViewAnimation = {
imageViewSnapshot.alpha = 1.0
}
let showImageViewDelay = 0.3
UIView.animateWithDuration(transitionDuration(transitionContext) - showImageViewDelay , delay: showImageViewDelay, options: UIViewAnimationOptions.CurveLinear, animations: showImageViewAnimation) { (completed) -> Void in
}
// Shrink the imageView to the original size
let scaleImageViewAnimation = {
imageViewSnapshot.frame = imageViewSnapshotOriginalFrame
}
UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0.0, options: UIViewAnimationOptions.CurveLinear, animations: scaleImageViewAnimation) { (completed) -> Void in
// After the complete animations have endet
// Hide ImageView after it is completely resized. Added some animation delay to not abruptly change the contactImage
if self.hideDelayed {
let hideImageViewAnimation = {
imageViewSnapshot.alpha = 0.0
}
UIView.animateWithDuration(0.2 , delay: 0.2, options: UIViewAnimationOptions.CurveLinear, animations: hideImageViewAnimation) { (completed) -> Void in
imageViewSnapshot.removeFromSuperview()
}
}
else {
imageViewSnapshot.removeFromSuperview()
}
}
}
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
if let transitionContext = self.transitionContext {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
}
// MARK: UIViewControllerTransitioningDelegate protocol methods
// return the animataor when presenting a viewcontroller
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
// return the animator used when dismissing from a viewcontroller
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
}
You need set new frame to toViewController.view. This will update the view to current orientation.
toViewController.view.frame = fromViewController.view.frame
Use my updated code :
class ZoomOutCircleViewTransition: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
var hideDelayed = false
var transitionContext: UIViewControllerContextTransitioning?
init(hideDelayed : Bool = true) {
self.hideDelayed = hideDelayed
super.init()
}
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.6
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
self.transitionContext = transitionContext
guard let toViewController: UIViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) else {
return
}
guard let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) else {
return
}
guard let toViewTransitionFromView = toViewController as? TransitionFromViewProtocol else {
return
}
toViewController.view.frame = fromViewController.view.frame
let containerView = transitionContext.containerView()
let imageViewSnapshot = toViewTransitionFromView.getViewForTransition()
imageViewSnapshot.alpha = 0.0
let imageViewSnapshotOriginalFrame = imageViewSnapshot.frame
let startFrame = CGRectMake(-CGRectGetWidth(fromViewController.view.frame)/2, -CGRectGetHeight(fromViewController.view.frame)/2, CGRectGetWidth(toViewController.view.frame)*2, CGRectGetHeight(toViewController.view.frame)*2)
let quadraticStartFrame = CGRect(x: startFrame.origin.x - (startFrame.height - startFrame.width)/2, y: startFrame.origin.y, width: startFrame.height, height: startFrame.height)
containerView!.insertSubview(toViewController.view, atIndex: 0)
containerView!.addSubview(imageViewSnapshot)
// UIViewController circle shrink animation
let bigCirclePath = UIBezierPath(ovalInRect: quadraticStartFrame)
let smallCirclePath = UIBezierPath(ovalInRect: imageViewSnapshot.frame)
let maskLayer = CAShapeLayer()
maskLayer.frame = toViewController.view.frame
maskLayer.path = bigCirclePath.CGPath//maskPath.CGPath
fromViewController.view.layer.mask = maskLayer
let pathAnimation = CABasicAnimation(keyPath: "path")
pathAnimation.delegate = self
pathAnimation.fromValue = bigCirclePath.CGPath
pathAnimation.toValue = smallCirclePath.CGPath
pathAnimation.duration = transitionDuration(transitionContext)
maskLayer.path = smallCirclePath.CGPath
maskLayer.addAnimation(pathAnimation, forKey: "pathAnimation")
// pathAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
imageViewSnapshot.frame = quadraticStartFrame
// Make imageView visible with animation
let showImageViewAnimation = {
imageViewSnapshot.alpha = 1.0
}
let showImageViewDelay = 0.3
UIView.animateWithDuration(transitionDuration(transitionContext) - showImageViewDelay , delay: showImageViewDelay, options: UIViewAnimationOptions.CurveLinear, animations: showImageViewAnimation) { (completed) -> Void in
}
// Shrink the imageView to the original size
let scaleImageViewAnimation = {
imageViewSnapshot.frame = imageViewSnapshotOriginalFrame
}
UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0.0, options: UIViewAnimationOptions.CurveLinear, animations: scaleImageViewAnimation) { (completed) -> Void in
// After the complete animations have endet
// Hide ImageView after it is completely resized. Added some animation delay to not abruptly change the contactImage
if self.hideDelayed {
let hideImageViewAnimation = {
imageViewSnapshot.alpha = 0.0
}
UIView.animateWithDuration(0.2 , delay: 0.2, options: UIViewAnimationOptions.CurveLinear, animations: hideImageViewAnimation) { (completed) -> Void in
imageViewSnapshot.removeFromSuperview()
}
}
else {
imageViewSnapshot.removeFromSuperview()
}
toViewController.view.layer.mask = nil
}
}
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
if let transitionContext = self.transitionContext {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
}
// MARK: UIViewControllerTransitioningDelegate protocol methods
// return the animataor when presenting a viewcontroller
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
// return the animator used when dismissing from a viewcontroller
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
}
class ZoomOutCircleViewTransition: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
var hideDelayed = false
var transitionContext: UIViewControllerContextTransitioning?
init(hideDelayed : Bool = true) {
self.hideDelayed = hideDelayed
super.init()
}
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.6
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
self.transitionContext = transitionContext
guard let toViewController: UIViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) else {
return
}
guard let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) else {
return
}
guard let toViewTransitionFromView = toViewController as? TransitionFromViewProtocol else {
return
}
toViewController.view.frame = fromViewController.view.frame
let containerView = transitionContext.containerView()
let imageViewSnapshot = toViewTransitionFromView.getViewForTransition()
imageViewSnapshot.alpha = 0.0
let imageViewSnapshotOriginalFrame = imageViewSnapshot.frame
let startFrame = CGRectMake(-CGRectGetWidth(fromViewController.view.frame)/2, -CGRectGetHeight(fromViewController.view.frame)/2, CGRectGetWidth(toViewController.view.frame)*2, CGRectGetHeight(toViewController.view.frame)*2)
let quadraticStartFrame = CGRect(x: startFrame.origin.x - (startFrame.height - startFrame.width)/2, y: startFrame.origin.y, width: startFrame.height, height: startFrame.height)
containerView!.insertSubview(toViewController.view, atIndex: 0)
containerView!.addSubview(imageViewSnapshot)
// UIViewController circle shrink animation
let bigCirclePath = UIBezierPath(ovalInRect: quadraticStartFrame)
let smallCirclePath = UIBezierPath(ovalInRect: imageViewSnapshot.frame)
let maskLayer = CAShapeLayer()
maskLayer.frame = toViewController.view.frame
maskLayer.path = bigCirclePath.CGPath//maskPath.CGPath
fromViewController.view.layer.mask = maskLayer
let pathAnimation = CABasicAnimation(keyPath: "path")
pathAnimation.delegate = self
pathAnimation.fromValue = bigCirclePath.CGPath
pathAnimation.toValue = smallCirclePath.CGPath
pathAnimation.duration = transitionDuration(transitionContext)
maskLayer.path = smallCirclePath.CGPath
maskLayer.addAnimation(pathAnimation, forKey: "pathAnimation")
// pathAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
imageViewSnapshot.frame = quadraticStartFrame
// Make imageView visible with animation
let showImageViewAnimation = {
imageViewSnapshot.alpha = 1.0
}
let showImageViewDelay = 0.3
UIView.animateWithDuration(transitionDuration(transitionContext) - showImageViewDelay , delay: showImageViewDelay, options: UIViewAnimationOptions.CurveLinear, animations: showImageViewAnimation) { (completed) -> Void in
}
// Shrink the imageView to the original size
let scaleImageViewAnimation = {
imageViewSnapshot.frame = imageViewSnapshotOriginalFrame
}
UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0.0, options: UIViewAnimationOptions.CurveLinear, animations: scaleImageViewAnimation) { (completed) -> Void in
// After the complete animations have endet
// Hide ImageView after it is completely resized. Added some animation delay to not abruptly change the contactImage
if self.hideDelayed {
let hideImageViewAnimation = {
imageViewSnapshot.alpha = 0.0
}
UIView.animateWithDuration(0.2 , delay: 0.2, options: UIViewAnimationOptions.CurveLinear, animations: hideImageViewAnimation) { (completed) -> Void in
imageViewSnapshot.removeFromSuperview()
}
}
else {
imageViewSnapshot.removeFromSuperview()
}
toViewController.view.layer.mask = nil
}
}
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
if let transitionContext = self.transitionContext {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
}
// MARK: UIViewControllerTransitioningDelegate protocol methods
// return the animataor when presenting a viewcontroller
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
// return the animator used when dismissing from a viewcontroller
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
}
I had the same problem and tried solution proposed by #Arun but it did not work. For me worked this:
guard let toViewController: UIViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) else { return }
let toFrame = transitionContext.finalFrameForViewController(toViewController)
and in completion block of animation:
}) { finished in
toViewController.view.frame = toFrame
transitionContext.completeTransition(finished)
}
Hope that helps!
Related
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)
}
}
}
I am doing a custom transition and if after present animation, device will be rotated and then destinationVC will be dismissed, originVC transform is not correct (not fulfil screen). If there is no device rotation, everything works perfectly fine. Does any one can help me?
Here is my code for present and dismiss animation:
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let originViewController = transitionContext.viewController(forKey: .from),
let destinationViewController = transitionContext.viewController(forKey: .to) else { return }
destinationViewController.view.transform = CGAffineTransform(translationX: 0, y: destinationViewController.view.frame.height)
let duration = transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration, animations: {
destinationViewController.view.transform = CGAffineTransform(translationX: 0, y: 0)
originViewController.view.transform = originViewController.view.transform.scaledBy(x: 0.95, y: 0.95)
originViewController.view.layer.cornerRadius = 8.0
}, completion: { completed in
transitionContext.completeTransition(completed)
})
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let originViewController = transitionContext.viewController(forKey: .from),
let destinationViewController = transitionContext.viewController(forKey: .to) else { return }
let duration = transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration, animations: {
originViewController.view.transform = CGAffineTransform(translationX: 0, y: destinationViewController.view.frame.height)
destinationViewController.view.transform = CGAffineTransform.identity
destinationViewController.view.layer.cornerRadius = 0.0
}, completion: { completed in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
Screens:
Before present animation
After present animation
After device rotation
After dismiss animation
EDIT:
when I add destinationViewController.view.frame = transitionContext.finalFrame(for: destinationViewController) to dismiss animation everything seems works fine but I don't know if this is right way
Add a subView in ViewC1 with leading, top, bottom, trailing constraints.
Working full code
class ViewC1: UIViewController {
#IBAction func presentNow(_ sender: Any) {
let viewc = storyboard!.instantiateViewController(withIdentifier: "ViewC2") as! ViewC2
viewc.transitioningDelegate = viewc
viewc.modalPresentationStyle = .overCurrentContext
present(viewc, animated: true, completion: nil)
}
}
class ViewC2: UIViewController {
let pres = PresentAnimator()
let diss = DismissAnimator()
#IBAction func dissmisNow(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
}
extension ViewC2: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return pres
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return diss
}
}
class PresentAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 1.0
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let originViewController = transitionContext.viewController(forKey: .from),
let destinationViewController = transitionContext.viewController(forKey: .to) else { return }
transitionContext.containerView.addSubview(destinationViewController.view)
destinationViewController.view.frame = transitionContext.containerView.bounds
let originView = originViewController.view.subviews.first
destinationViewController.view.transform = CGAffineTransform(translationX: 0, y: destinationViewController.view.frame.height)
let duration = transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration, animations: {
destinationViewController.view.transform = CGAffineTransform(translationX: 0, y: 0)
originView?.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
originView?.layer.cornerRadius = 8.0
}, completion: { completed in
transitionContext.completeTransition(completed)
})
}
}
class DismissAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 1.0
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let originViewController = transitionContext.viewController(forKey: .from),
let destinationViewController = transitionContext.viewController(forKey: .to) else { return }
let originView = destinationViewController.view.subviews.first
let duration = transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration, animations: {
originViewController.view.transform = CGAffineTransform(translationX: 0, y: destinationViewController.view.frame.height)
originView?.transform = CGAffineTransform.identity
originView?.layer.cornerRadius = 0.0
}, completion: { completed in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
Update:
or you can override willRotate and didRotate if you dont want to use view.subviews.first
var prevTrans: CGAffineTransform?
override func willRotate(to toInterfaceOrientation: UIInterfaceOrientation, duration: TimeInterval) {
prevTrans = view.transform
view.transform = CGAffineTransform.identity
}
override func didRotate(from fromInterfaceOrientation: UIInterfaceOrientation) {
if let prevTrans = prevTrans {
view.transform = prevTrans
}
}
I built a class to implement a circular transition between view controllers. When I hit the button to navigate to the other view controller a circle starts growing from the button until it fills the screen with the new controller. When I dismiss the view controller I expected this circle to shrink down back to the original position. It's also working. The only problem is that when the dismiss is underway the back of the screen while the circle is shrinking is completely black and after the animation is completed the new viewController appears abruptly.
Here are some photos of the effect:
Here's the code of the custom class:
class customTransition: NSObject, UIViewControllerAnimatedTransitioning{
var duration: TimeInterval = 0.5
var startPoint = CGPoint.zero
var circle = UIView()
var circleColor = UIColor.white
enum transitMode: Int {
case presenting, dismissing
}
var transitionMode: transitMode = .presenting
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let container = transitionContext.containerView
guard let to = transitionContext.view(forKey: UITransitionContextViewKey.to) else {return}
guard let from = transitionContext.view(forKey: UITransitionContextViewKey.from) else {return}
circleColor = to.backgroundColor ?? UIColor.white
if transitionMode == .presenting {
to.translatesAutoresizingMaskIntoConstraints = false
to.center = startPoint
circle = UIView()
circle.backgroundColor = circleColor
circle.frame = getFrameForCircle(rect: to.frame)
circle.layer.cornerRadius = circle.frame.width / 2
circle.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
circle.alpha = 0
circle.addSubview(to)
to.centerXAnchor.constraint(equalTo: circle.centerXAnchor).isActive = true
to.centerYAnchor.constraint(equalTo: circle.centerYAnchor).isActive = true
to.widthAnchor.constraint(equalToConstant: to.frame.width).isActive = true
to.heightAnchor.constraint(equalToConstant: to.frame.height).isActive = true
container.addSubview(circle)
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: UIView.AnimationOptions.curveLinear, animations: {
self.circle.center = from.center
self.circle.transform = CGAffineTransform.identity
self.circle.alpha = 1
}) { (sucess) in
transitionContext.completeTransition(sucess)
}
} else if transitionMode == .dismissing {
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: UIView.AnimationOptions.curveLinear, animations: {
self.circle.center = self.startPoint
self.circle.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
self.circle.alpha = 0
}) { (sucess) in
transitionContext.completeTransition(sucess)
}
}
}
func getFrameForCircle(rect: CGRect) -> CGRect{
let width = Float(rect.width)
let height = Float(rect.height)
let diameter = CGFloat(sqrtf(width * width + height * height))
let x: CGFloat = rect.midX - (diameter / 2)
let y: CGFloat = rect.midY - (diameter / 2)
return CGRect(x: x, y: y, width: diameter, height: diameter)
}
}
and the implementation...
let circularTransition = customTransition()
the call for the present view controller... I tried to set secondVC.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext but when I set this line it ignores completely the animation transition I don't know why...
`
#objc func handlePresent(sender: UIButton){
let secondVC = nextVC()
secondVC.transitioningDelegate = self
present(secondVC, animated: true, completion: nil)
}
delegate methods:
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
circularTransition.startPoint = presentButton.center
circularTransition.transitionMode = .presenting
return circularTransition
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
circularTransition.transitionMode = .dismissing
circularTransition.startPoint = presentButton.center
return circularTransition
}
What am I missing here? Any suggestions?
No storyboard being used, just code.
If you don't use navigationController, it's necessary to use the .custom mode in the presentedviewController.
import UIKit
class TransViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
let circularTransition = customTransition()
#IBOutlet var presentButton : UIButton!
#IBAction func handlePresent(sender: UIButton){
if let secondVC = storyboard?.instantiateViewController(withIdentifier: "next"){
secondVC.modalPresentationStyle = .custom
secondVC.transitioningDelegate = self
present(secondVC, animated: true, completion: nil)
}
}
}
class BackViewController: UIViewController {
#IBAction func dismissMe(sender: UIButton){
self.dismiss(animated: true, completion: nil)
}
}
extension TransViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
circularTransition.startPoint = presentButton.center
circularTransition.transitionMode = .presenting
return circularTransition
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
circularTransition.transitionMode = .dismissing
circularTransition.startPoint = presentButton.center
return circularTransition
}
}
If there is no from or to view, we have use the from and to view from containView.
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let container = transitionContext.containerView
var to : UIView!
var from : UIView!
to = transitionContext.view(forKey: UITransitionContextViewKey.to)
if to == nil {to = container}
from = transitionContext.view(forKey: UITransitionContextViewKey.from)
if from == nil {from = container}
The rest is same:
circleColor = to.backgroundColor ?? UIColor.white
if transitionMode == .presenting {
to.translatesAutoresizingMaskIntoConstraints = false
to.center = startPoint
circle = UIView()
circle.backgroundColor = circleColor
circle.frame = getFrameForCircle(rect: to.frame)
circle.layer.cornerRadius = circle.frame.width / 2
circle.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
circle.alpha = 0
circle.addSubview(to)
to.centerXAnchor.constraint(equalTo: circle.centerXAnchor).isActive = true
to.centerYAnchor.constraint(equalTo: circle.centerYAnchor).isActive = true
to.widthAnchor.constraint(equalToConstant: to.frame.width).isActive = true
to.heightAnchor.constraint(equalToConstant: to.frame.height).isActive = true
container.addSubview(circle)
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: UIView.AnimationOptions.curveLinear, animations: {
self.circle.center = from.center
self.circle.transform = CGAffineTransform.identity
self.circle.alpha = 1
}) { (sucess) in
transitionContext.completeTransition(sucess)
}
} else if transitionMode == .dismissing {
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: UIView.AnimationOptions.curveLinear, animations: {
self.circle.center = self.startPoint
self.circle.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
self.circle.alpha = 0
}) { (sucess) in
transitionContext.completeTransition(sucess)
}
}
}
I am trying to do Transition Animation like in snapchat.When user swaps screen in downward direction the video continues to play smoothly.
Image in container While transition is performed
I am adding FromViewController view as transition container subview.
(FromViewController plays video using AVPlayerLayer)
but while transition the video in the container layer (which i have added FromViewController ) lags it does not play smoothly.
containerView!.addSubview((fromVC?.view)!)
I am posting the code below.
class CircleTransitionAnimator: NSObject , UIViewControllerAnimatedTransitioning
{
weak var transitionContext: UIViewControllerContextTransitioning?
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.5;
}
#IBAction func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
self.transitionContext = transitionContext
let containerView = transitionContext.containerView()
let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) //as! ViewController
let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) //as! ViewController
containerView!.addSubview((fromVC?.view)!)
let initialFrame = CGRectMake(-fromVC!.view.frame.size.width/2,0, fromVC!.view.frame.size.height, fromVC!.view.frame.size.height ) as CGRect
var endFrame : CGRect = CGRectMake(0, 0, 0, 0)
if toVC?.isKindOfClass(ViewController) == true {
let viewToMove = toVC as! ViewController
let cellForDismiss = viewToMove.cellViewToDismissTo
if cellForDismiss != nil {
endFrame = cellForDismiss.frame as CGRect
}
self.transitionContext?.initialFrameForViewController(fromVC!)
let maskPath = UIBezierPath(rect: initialFrame)
let maskLayer = CAShapeLayer()
maskLayer.frame = toVC!.view.frame;
maskLayer.path = maskPath.CGPath;
let smallCirclePath = UIBezierPath(rect: endFrame)
maskLayer.path = smallCirclePath.CGPath;
fromVC!.view.layer.mask = maskLayer
let pathAnimation = CABasicAnimation(keyPath: "path")
pathAnimation.fromValue = maskPath.CGPath
pathAnimation.toValue = smallCirclePath
pathAnimation.duration = self.transitionDuration(transitionContext)
let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = 1.0;
opacityAnimation.toValue = 0.5;
opacityAnimation.duration = self.transitionDuration(transitionContext)
CATransaction.begin()
CATransaction.setCompletionBlock({
transitionContext.completeTransition(transitionContext.transitionWasCancelled())
toVC!.view.layer.mask = nil
fromVC!.view.layer.mask = nil
sourceSnap.removeFromSuperview()
})
maskLayer.addAnimation(pathAnimation, forKey: "pathAnimation")
maskLayer.addAnimation(opacityAnimation, forKey: "opacityAnimation")
CATransaction.commit()
}
else{
if fromVC?.isKindOfClass(ViewController) == true {
let viewToMove = fromVC as! ViewController
let cellForDismiss = viewToMove.cellViewToDismissTo
containerView!.addSubview(toVC!.view)
let circleMaskPathInitial = UIBezierPath(rect: cellForDismiss!.frame)
let extremePoint = CGPoint(x: cellForDismiss!.center.x - 0, y: cellForDismiss!.center.y - CGRectGetHeight(toVC!.view.bounds))
let radius = sqrt((extremePoint.x*extremePoint.x) + (extremePoint.y*extremePoint.y))
let circleMaskPathFinal = UIBezierPath(rect: CGRectInset(cellForDismiss!.frame, -radius, -radius))
let maskLayer = CAShapeLayer()
maskLayer.path = circleMaskPathFinal.CGPath
toVC!.view.layer.mask = maskLayer
let maskLayerAnimation = CABasicAnimation(keyPath: "path")
maskLayerAnimation.fromValue = circleMaskPathInitial.CGPath
maskLayerAnimation.toValue = circleMaskPathFinal.CGPath
maskLayerAnimation.duration = self.transitionDuration(transitionContext)
maskLayerAnimation.delegate = self
maskLayer.addAnimation(maskLayerAnimation, forKey: "path")
}
}
}
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
self.transitionContext?.completeTransition(!self.transitionContext!.transitionWasCancelled())
self.transitionContext?.viewControllerForKey(UITransitionContextFromViewControllerKey)?.view.layer.mask = nil
}
}
In the other class i am handling pan gesture states.
class NavigationControllerDelegate: NSObject , UINavigationControllerDelegate {
#IBOutlet weak var navigationController: UINavigationController?
var interactionController: UIPercentDrivenInteractiveTransition?
var transitionInProgress : Bool = false
var shouldCompleteTransition : Bool = false
var parentViewController : UIViewController!
var percent : CGFloat!
override func awakeFromNib() {
super.awakeFromNib()
// Added Pan gesture to Navigatin Controller
let panGesture = UIPanGestureRecognizer(target: self, action: Selector("panned:"))
self.navigationController!.view.addGestureRecognizer(panGesture)
let controller :[UIViewController] = (self.navigationController?.viewControllers)!
for contro in controller {
if contro.isKindOfClass(ViewController) == true {
parentViewController = contro
}
}
}
#IBAction func panned(gestureRecognizer: UIPanGestureRecognizer) {
let point : CGPoint = gestureRecognizer .translationInView(self.parentViewController.view)
self.percent = CGFloat( fmaxf(fminf(Float(point.y) / 400.0 , 0.99), 0.0))
let velocity : CGPoint = gestureRecognizer.velocityInView(self.parentViewController.view)
// Percent is used for completing transaction , its value tells when the complete transition should start ( after the point when we stop sliding the view down )
switch gestureRecognizer.state {
case .Began:
//
self.transitionInProgress = true;
self.interactionController = UIPercentDrivenInteractiveTransition()
self.navigationController?.popViewControllerAnimated(true)
case .Changed:
// when we use swipe down then this block is continously called and update the interactve transition
self.shouldCompleteTransition = (Double(self.percent) > 0.0 );
let translation = gestureRecognizer.translationInView(self.navigationController!.view)
let completionProgress = translation.x/CGRectGetWidth(self.navigationController!.view.bounds)
self.interactionController?.updateInteractiveTransition(self.percent)
case .Ended:
// When swipe gesture ends , we finish the transition or in any other case "cancel" the transition
self.transitionInProgress = false;
if (self.shouldCompleteTransition == false || gestureRecognizer.state == .Cancelled) {
self.interactionController?.finishInteractiveTransition()
}else{
self.interactionController?.cancelInteractiveTransition()
}
self.interactionController = nil
default:
self.interactionController?.cancelInteractiveTransition()
self.interactionController = nil
}
}
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CircleTransitionAnimator()
}
func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return self.interactionController
}
}
I have created a transition and it is working fine except that I get sometime black corners in the simulator. Additionally in iPad Pro I get a completely black screen if I run the simulator in full resolution. The resized resolutions work fine. Do you have an idea what might be the problem?
Another thing that I recognized is that the content behind the black screen is there and responds to touches. E.g. on a touch I reload the cell of a collectionview. Then this cell is visible while the rest of the collectionview is black.
class ZoomInCircleViewTransition: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
var transitionContext: UIViewControllerContextTransitioning?
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.6
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
self.transitionContext = transitionContext
guard let toViewController: UIViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) else {
return
}
guard let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) else { return
}
guard let fromViewTransitionFromView = fromViewController as? TransitionFromViewProtocol else {
return
}
let imageViewSnapshot = fromViewTransitionFromView.getViewForTransition()
let endFrame = CGRectMake(-CGRectGetWidth(toViewController.view.frame)/2, -CGRectGetHeight(toViewController.view.frame)/2, CGRectGetWidth(toViewController.view.frame)*2, CGRectGetHeight(toViewController.view.frame)*2)
if let containerView = transitionContext.containerView(){
containerView.addSubview(fromViewController.view)
containerView.addSubview(toViewController.view)
containerView.addSubview(imageViewSnapshot)
}
let maskPath = UIBezierPath(ovalInRect: imageViewSnapshot.frame)
let maskLayer = CAShapeLayer()
maskLayer.frame = toViewController.view.frame
maskLayer.path = maskPath.CGPath
toViewController.view.layer.mask = maskLayer
let quadraticEndFrame = CGRect(x: endFrame.origin.x - (endFrame.height - endFrame.width)/2, y: endFrame.origin.y, width: endFrame.height, height: endFrame.height)
let bigCirclePath = UIBezierPath(ovalInRect: quadraticEndFrame)
let pathAnimation = CABasicAnimation(keyPath: "path")
pathAnimation.delegate = self
pathAnimation.fromValue = maskPath.CGPath
pathAnimation.toValue = bigCirclePath
pathAnimation.duration = transitionDuration(transitionContext)
maskLayer.path = bigCirclePath.CGPath
maskLayer.addAnimation(pathAnimation, forKey: "pathAnimation")
let hideImageViewAnimation = {
imageViewSnapshot.alpha = 0.0
}
UIView.animateWithDuration(0.2, delay: 0.0, options: UIViewAnimationOptions.CurveLinear, animations: hideImageViewAnimation) { (completed) -> Void in
}
let scaleImageViewAnimation = {
imageViewSnapshot.frame = quadraticEndFrame
}
UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0.0, options: UIViewAnimationOptions.CurveLinear, animations: scaleImageViewAnimation) { (completed) -> Void in
// After the complete animations hav endet
imageViewSnapshot.removeFromSuperview()
}
}
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
if let transitionContext = self.transitionContext {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
}
// MARK: UIViewControllerTransitioningDelegate protocol methods
// return the animataor when presenting a viewcontroller
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
// return the animator used when dismissing from a viewcontroller
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
}
You need remove mask layer once your are done with custom transition animations.
toViewController.view.layer.mask = nil
Please use this updated code:
class ZoomInCircleViewTransition: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
var transitionContext: UIViewControllerContextTransitioning?
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.6
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
self.transitionContext = transitionContext
guard let toViewController: UIViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) else {
return
}
guard let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) else { return
}
guard let fromViewTransitionFromView = fromViewController as? TransitionFromViewProtocol else {
return
}
let imageViewSnapshot = fromViewTransitionFromView.getViewForTransition()
let endFrame = CGRectMake(-CGRectGetWidth(toViewController.view.frame)/2, -CGRectGetHeight(toViewController.view.frame)/2, CGRectGetWidth(toViewController.view.frame)*2, CGRectGetHeight(toViewController.view.frame)*2)
if let containerView = transitionContext.containerView(){
containerView.addSubview(fromViewController.view)
containerView.addSubview(toViewController.view)
containerView.addSubview(imageViewSnapshot)
}
let maskPath = UIBezierPath(ovalInRect: imageViewSnapshot.frame)
let maskLayer = CAShapeLayer()
maskLayer.frame = toViewController.view.frame
maskLayer.path = maskPath.CGPath
toViewController.view.layer.mask = maskLayer
let quadraticEndFrame = CGRect(x: endFrame.origin.x - (endFrame.height - endFrame.width)/2, y: endFrame.origin.y, width: endFrame.height, height: endFrame.height)
let bigCirclePath = UIBezierPath(ovalInRect: quadraticEndFrame)
let pathAnimation = CABasicAnimation(keyPath: "path")
pathAnimation.delegate = self
pathAnimation.fromValue = maskPath.CGPath
pathAnimation.toValue = bigCirclePath
pathAnimation.duration = transitionDuration(transitionContext)
maskLayer.path = bigCirclePath.CGPath
maskLayer.addAnimation(pathAnimation, forKey: "pathAnimation")
let hideImageViewAnimation = {
imageViewSnapshot.alpha = 0.0
}
UIView.animateWithDuration(0.2, delay: 0.0, options: UIViewAnimationOptions.CurveLinear, animations: hideImageViewAnimation) { (completed) -> Void in
}
let scaleImageViewAnimation = {
imageViewSnapshot.frame = quadraticEndFrame
}
UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0.0, options: UIViewAnimationOptions.CurveLinear, animations: scaleImageViewAnimation) { (completed) -> Void in
// After the complete animations hav endet
imageViewSnapshot.removeFromSuperview()
toViewController.view.layer.mask = nil
}
}
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
if let transitionContext = self.transitionContext {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
}
// MARK: UIViewControllerTransitioningDelegate protocol methods
// return the animataor when presenting a viewcontroller
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
// return the animator used when dismissing from a viewcontroller
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
}
In my case, I had set the presentation style and delegate in the incorrect place. When placed in viewDidLoad(), the transitioningDelegate is honored, but the modalPresentationStyle is not — resulting in a black screen. I had to move the style to be set earlier on init.
class MyViewController: UIViewController {
init() {
super.init(nibName: String(describing: type(of: self)), bundle: .main)
transitioningDelegate = self
modalPresentationStyle = .custom
}
#available(*, unavailable)
required init?(coder aDecoder: NSCoder) {
fatalError()
}
override func viewDidLoad() {
super.viewDidLoad()
// I incorrectly had the delegate and presentationStyle set here.
}
}
extension MyViewController: UIViewControllerTransitioningDelegate {
//...
}
Calling class:
let vc = MyViewController()
present(vc, animated: true)