iOS Swift: Custom interactive transition - ios

I wrote a custom swipe transition that works fine on a modal presentation. But in a push presentation the "to" view position is not animating.
I've tried the same code with switching the translation with alpha and it works.
The from view works perfectly, it's just the to view that stays fixed during the animation.
func transitionAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
let duration = transitionDuration(using: transitionContext)
let container = transitionContext.containerView
let toController = transitionContext.viewController(forKey: .to)
toController?.beginAppearanceTransition(true, animated: true)
guard
let to = transitionContext.view(forKey: .to),
let from = transitionContext.view(forKey: .from)
else {
print("To or from view are nil!")
fatalError()
}
container.addSubview(to)
let animator = UIViewPropertyAnimator(duration: duration, curve: .linear)
var toStartingPoint: CGPoint
var fromEndingPoint: CGPoint
switch self.from {
case .down:
toStartingPoint = CGPoint(x: 0, y: -from.bounds.height)
fromEndingPoint = CGPoint(x: 0, y: from.bounds.height)
case .top:
toStartingPoint = CGPoint(x: 0, y: from.bounds.height)
fromEndingPoint = CGPoint(x: 0, y: -from.bounds.height)
case .right:
toStartingPoint = CGPoint(x: from.bounds.width, y: 0)
fromEndingPoint = CGPoint(x: -from.bounds.width, y: 0)
case .left:
toStartingPoint = CGPoint(x: -from.bounds.width, y: 0)
fromEndingPoint = CGPoint(x: from.bounds.width, y: 0)
}
to.transform = CGAffineTransform(translationX: toStartingPoint.x, y: toStartingPoint.y)
animator.addAnimations({
from.transform = CGAffineTransform(translationX: fromEndingPoint.x, y: fromEndingPoint.y)
}, delayFactor: 0.0)
animator.addAnimations({
to.transform = .identity
}, delayFactor: 0.0)
animator.addCompletion { [weak self] position in
switch position {
case .start:
self?.auxCancelCompletion?()
transitionContext.completeTransition(false)
self?.auxAnimationsCancel?()
case .end:
self?.auxEndCompletion?()
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
from.transform = .identity
to.transform = .identity
default:
transitionContext.completeTransition(false)
self?.auxAnimationsCancel?()
}
}
if let auxAnimations = auxAnimations {
animator.addAnimations(auxAnimations)
}
self.animator = animator
self.context = transitionContext
animator.addCompletion { [unowned self] _ in
self.animator = nil
self.context = nil
}
animator.isUserInteractionEnabled = true
return animator
}
I was thinking that was a problem about delegates but the navigationDelgate is correctly set, otherwise I think I wouldn't see any animation..
Delegate setting:
override func viewDidLoad() {
super.viewDidLoad()
transitionHelper = SwipeInteractiveTransitionHelper(withDelegate: self)
}
extension TodayViewController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return transitionHelper?.swipeTransition
}
func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return transitionHelper?.swipeTransition
}
}
and here is the custom push coordinator, where the viewController is the next view controller, and where I attach the delegate.
case .pushCustom:
guard let navigationController = currentViewController.navigationController else {
fatalError("Can't push a view controller without a current navigation controller")
}
guard let current = currentViewController as? UINavigationControllerDelegate else {
fatalError("Can't push a view controller without a current navigation delegate")
}
navigationController.delegate = current
navigationController.pushViewController(viewController, animated: true) { [weak self] in
self?.currentViewController = SceneCoordinator.actualViewController(for: viewController)
completion?()
}

Solved animating a snapshot of the destination view, instead of directly animating the destination view.
let to = transitionContext.view(forKey: .to)
let toViewSnapshot = to.snapshotView(afterScreenUpdates: true)
Just use the toViewSnapshot for the animation

What I did is this way. Hope it will help you.
VC:UIViewController
{
#IBAction func test(_ sender: Any){
navigationController?.delegate = self
let destine = storyboard?.instantiateViewController(withIdentifier: "target")
navigationController?.pushViewController(destine!, animated: true)
}
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?{
return Traistion()
}
}
class Traistion: NSObject, UIViewControllerAnimatedTransitioning{
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let animator = transitionAnimator(using: transitionContext)
animator.startAnimation()
}
var animator: UIViewPropertyAnimator!
var context: UIViewControllerContextTransitioning!
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval{
return 1.0
}
enum Direction {
case down
case top
case left
case right
}
var from : Direction = .left
var auxCancelCompletion :(()->())? = {
return nil
}()
var auxAnimationsCancel :(()->())? = {
return nil
}()
var auxEndCompletion :(()->())? = {
return nil
}()
var auxAnimations : (()->())?
func transitionAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
let duration = transitionDuration(using: transitionContext)
let container = transitionContext.containerView
let toController = transitionContext.viewController(forKey: .to)
toController?.beginAppearanceTransition(true, animated: true)
guard
let to = transitionContext.view(forKey: .to),
let from = transitionContext.view(forKey: .from)
else {
print("To or from view are nil!")
fatalError()
}
container.addSubview(to)
let animator = UIViewPropertyAnimator(duration: duration, curve: .linear)
var toStartingPoint: CGPoint
var fromEndingPoint: CGPoint
switch self.from {
case .down:
toStartingPoint = CGPoint(x: 0, y: -from.bounds.height)
fromEndingPoint = CGPoint(x: 0, y: from.bounds.height)
case .top:
toStartingPoint = CGPoint(x: 0, y: from.bounds.height)
fromEndingPoint = CGPoint(x: 0, y: -from.bounds.height)
case .right:
toStartingPoint = CGPoint(x: from.bounds.width, y: 0)
fromEndingPoint = CGPoint(x: -from.bounds.width, y: 0)
case .left:
toStartingPoint = CGPoint(x: -from.bounds.width, y: 0)
fromEndingPoint = CGPoint(x: from.bounds.width, y: 0)
}
to.transform = CGAffineTransform(translationX: toStartingPoint.x, y: toStartingPoint.y)
animator.addAnimations({
from.transform = CGAffineTransform(translationX: fromEndingPoint.x, y: fromEndingPoint.y)
}, delayFactor: 0.0)
animator.addAnimations({
to.transform = .identity
}, delayFactor: 0.0)
animator.addCompletion { [weak self] position in
switch position {
case .start:
self?.auxCancelCompletion?()
transitionContext.completeTransition(false)
self?.auxAnimationsCancel?()
case .end:
self?.auxEndCompletion?()
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
from.transform = .identity
to.transform = .identity
default:
transitionContext.completeTransition(false)
self?.auxAnimationsCancel?()
}
}
if let auxAnimations = auxAnimations {
animator.addAnimations(auxAnimations)
}
self.animator = animator
self.context = transitionContext
animator.addCompletion { [unowned self] _ in
//self.animator = nil
// self.context = nil
}
animator.isUserInteractionEnabled = true
return animator
}
}
When use interruptibleAnimator. It will call this function at least twice and you are supposed to provide the same animator but different one. So you have to call it like this way:
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
animator = transitionAnimator(using: transitionContext) as! UIViewPropertyAnimator
animator.startAnimation()
}
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
return animator
}
var animator: UIViewPropertyAnimator!
var context: UIViewControllerContextTransitioning!
or simpler like this:
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
}
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
if animator == nil {animator = transitionAnimator(using: transitionContext) as! UIViewPropertyAnimator
animator.startAnimation()}
return animator
}
Here animator will be called twice and which is the same one. If
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
return transitionAnimator(using: transitionContext) }
This is not the right way to use as the animator will be different.
https://developer.apple.com/documentation/uikit/uiviewcontrolleranimatedtransitioning/1829434-interruptibleanimator
See the last line in the document.

Related

How to disable UINavigationBar Animation when the UIPercentDrivenInteractiveTransition is cancelled

I have implemented a custom interactive transition for detecting the middle screen swipe with the UIPanGestureRecognizer along with UIPercentDrivenInteractiveTransition and animating it with UIViewControllerAnimatedTransitioning.
For now, I have a problem when the UIPercentDrivenInteractiveTransition was cancelled but the UINavigationBar pop animation was still animated like this
Example
Is there anyway to disable or cancel the animation when there are some situation like this?
Here is my code https://github.com/kanottonp/NavBarBugPOC
ViewController.swift
#IBOutlet weak var button: UIButton!
static var count = 1
private var percentDrivenInteractiveTransition: UIPercentDrivenInteractiveTransition!
private var panGestureRecognizer: UIPanGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
addGesture()
self.navigationController?.setNavigationBarHidden(false, animated: true)
self.title = "Hello \(ViewController.count)"
ViewController.count += 1
}
#IBAction func onTouch(_ sender: Any) {
guard let newVC = storyboard?.instantiateViewController(withIdentifier: "ViewController") else {
return
}
self.navigationController?.pushViewController(newVC, animated: true)
}
private func addGesture() {
guard panGestureRecognizer == nil else {
return
}
guard self.navigationController?.viewControllers.count > 1 else {
return
}
panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePanGesture(_:)))
panGestureRecognizer.cancelsTouchesInView = true;
panGestureRecognizer.delaysTouchesBegan = true;
panGestureRecognizer.maximumNumberOfTouches = 1
self.view.addGestureRecognizer(panGestureRecognizer)
self.navigationController?.interactivePopGestureRecognizer?.delegate = panGestureRecognizer as? UIGestureRecognizerDelegate
}
#objc private func handlePanGesture(_ panGesture: UIPanGestureRecognizer) {
let percent = max(panGesture.translation(in: view).x, 0) / view.frame.width
switch panGesture.state {
case .began:
navigationController?.delegate = self
if panGesture.velocity(in: view).x > 0 {
_ = navigationController?.popViewController(animated: true)
}
case .changed:
if let percentDrivenInteractiveTransition = percentDrivenInteractiveTransition {
percentDrivenInteractiveTransition.update(percent)
}
case .ended:
let velocity = panGesture.velocity(in: view).x
// Continue if drag more than 50% of screen width or velocity is higher than 300
if let percentDrivenInteractiveTransition = percentDrivenInteractiveTransition {
if percent > 0.5 || velocity > 300 {
percentDrivenInteractiveTransition.finish()
} else {
percentDrivenInteractiveTransition.cancel()
}
}
case .cancelled, .failed:
percentDrivenInteractiveTransition.cancel()
default:
break
}
}
extension ViewController: UINavigationControllerDelegate
public func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return SlideAnimatedTransitioning()
}
public func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
navigationController.delegate = nil
if panGestureRecognizer.state == .began && panGestureRecognizer.velocity(in: view).x > 0 {
percentDrivenInteractiveTransition = UIPercentDrivenInteractiveTransition()
percentDrivenInteractiveTransition.completionCurve = .easeInOut
} else {
percentDrivenInteractiveTransition = nil
}
return percentDrivenInteractiveTransition
}
SlideAnimatedTransitioning.swift
extension SlideAnimatedTransitioning: UIViewControllerAnimatedTransitioning
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// use animator to implement animateTransition
let animator = interruptibleAnimator(using: transitionContext)
animator.startAnimation()
}
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
if let propertyAnimator = propertyAnimator {
return propertyAnimator
}
let containerView = transitionContext.containerView
let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
let fromView = transitionContext.view(forKey: .from)!
let toView = transitionContext.view(forKey: .to)!
toView.frame = transitionContext.finalFrame(for: toViewController)
toView.frame = CGRect(x: toView.frame.origin.x, y: toView.frame.origin.y, width: toView.frame.size.width, height: toView.frame.size.height + toView.frame.origin.y)
let width = containerView.frame.width
var offsetLeft = fromView.frame
offsetLeft.origin.x = width
var offscreenRight = toView.frame
offscreenRight.origin.x = -width / 3.33;
toView.frame = offscreenRight;
toView.layer.opacity = 0.9
containerView.insertSubview(toView, belowSubview: fromView)
let animator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), timingParameters: UICubicTimingParameters(animationCurve: .easeInOut))
animator.addAnimations {
toView.frame = CGRect(x: fromView.frame.origin.x, y: toView.frame.origin.y, width: toView.frame.width, height: toView.frame.height)
fromView.frame = offsetLeft
toView.layer.opacity = 1.0
}
animator.addCompletion { (success) in
toView.layer.opacity = 1.0
fromView.layer.opacity = 1.0
fromViewController.navigationItem.titleView?.layer.opacity = 1
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
self.propertyAnimator = nil
}
self.propertyAnimator = animator
return animator
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
if transitionContext?.transitionWasCancelled == true { return 0 }
return 2
}

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)
}
}
}

CGAffineTransform.identity doesn't reset transform correctly after device rotation

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
}
}

How to solve this problem with custom transition in iOS?

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)
}
}
}

Screen flashes black when using custom animation controller to dismiss

I have written a custom animation controller to animate my view controller transition. Code as follows :
class PopupAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
var backgroundColorView: UIView!
let duration: TimeInterval = 0.35
var dismiss = false
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
if !dismiss {
animatePresentTransition(using: transitionContext)
} else {
animateDismissTransition(using: transitionContext)
}
}
func animatePresentTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) else {
return
}
let containerView = transitionContext.containerView
let finalFrameForVC = transitionContext.finalFrame(for: toVC) // What is this about?
let bounds = UIScreen.main.bounds
let snapshotView = fromVC.view.snapshotView(afterScreenUpdates: false)
snapshotView?.frame = bounds
backgroundColorView = UIView(frame: bounds)
backgroundColorView.backgroundColor = UIColor.black.withAlphaComponent(0)
// Put the window at the bottom of the screen
toVC.view.frame = finalFrameForVC.offsetBy(dx: 0, dy: bounds.size.height)
// Layer out the views
containerView.addSubview(snapshotView!)
containerView.addSubview(backgroundColorView)
containerView.addSubview(toVC.view)
// The animation happens here
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0, options: [], animations: {
self.backgroundColorView.backgroundColor = UIColor.black.withAlphaComponent(0.4)
toVC.view.frame = finalFrameForVC
}, completion: {
finished in
transitionContext.completeTransition(true)
})
}
func animateDismissTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) else {
return
}
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0, options: [], animations: {
self.backgroundColorView.backgroundColor = UIColor.black.withAlphaComponent(0.0)
let bounds = UIScreen.main.bounds
fromVC.view.frame = fromVC.view.frame.offsetBy(dx: 0, dy: bounds.size.height)
}, completion: {
finished in
transitionContext.completeTransition(true)
})
}
}
I have implemented the UIViewControllerTransitioningDelegate methods in the controller that will use this transition like so :
extension PopupViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transitionAnimator.dismiss = false
return transitionAnimator
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transitionAnimator.dismiss = true
return transitionAnimator
}
}
When I'm not using a spring in the animation (eg damping set to 1) it works perfectly. However if I include spring in the animation like in this example the screen flashes black when the controller is dismissed:
Not sure why this is happening. Any ideas on what I might be doing wrong where? Any pointers would be greatly appreciated!

Resources