CABasicAnimation with UINavigatonController and custom animations goes wrong - ios

Here below you can see the result of my code. Please note that the second time when I press in the Back Button the animation is not working.
Best Regards
I have tried for hours to understand what is the trouble with my animation, but I can't find the source of the problem. I have been looking around the internet for an answer without success. So, this is my problem.
I put the animation to finish() if the transition goes more than 50% of the view.bounds.width and cancel() otherwise
This is my steps
Handle the Animation to less than 50%
The animations goes back
Press the Back button in the UINavigationItem and the animation runs to fast
This my code in the Animator
class Animator: UIPercentDrivenInteractiveTransition, UIViewControllerAnimatedTransitioning {
private var pausedTime: CFTimeInterval = 0
private var isLayerBased: Bool { operation == .pop }
let animationDuration = 1.0
weak var storedContext: UIViewControllerContextTransitioning?
var interactive: Bool = false
var operation: UINavigationController.Operation = .push
func handlePan(recognizer: UIScreenEdgePanGestureRecognizer)
{
// This part is only for get percent of the translation in the screen with the finger
let translation = recognizer.translation(in: recognizer.view!.superview!)
var progress: CGFloat = abs(translation.x / recognizer.view!.superview!.bounds.width)
progress = min(max(progress, 0.01), 0.99)
switch recognizer.state {
case .changed:
update(progress)
case .cancelled, .ended:
if progress < 0.5 {
cancel()
} else {
finish()
}
interactive = false
default:
break
}
}
override func update(_ percentComplete: CGFloat) {
super.update(percentComplete)
if isLayerBased {
let animationProgress = TimeInterval(animationDuration) * TimeInterval(percentComplete)
storedContext?.containerView.layer.timeOffset = pausedTime + animationProgress
}
}
override func cancel() {
if isLayerBased {
restart(forFinishing: false)
}
super.cancel()
}
override func finish() {
if isLayerBased {
restart(forFinishing: true)
}
super.finish()
}
private func restart(forFinishing: Bool)
{
let transitionLayer = storedContext?.containerView.layer
transitionLayer?.beginTime = CACurrentMediaTime()
transitionLayer?.speed = forFinishing ? 1 : -1
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
{
if interactive && isLayerBased {
let transitionLayer = transitionContext.containerView.layer
self.pausedTime = transitionLayer.convertTime(CACurrentMediaTime(), from: nil)
transitionLayer.speed = 0
transitionLayer.timeOffset = pausedTime
}
self.storedContext = transitionContext
if operation == .pop {
let fromVC = transitionContext.viewController(forKey: .from)!
let toVC = transitionContext.viewController(forKey: .to)!
let animation = CABasicAnimation(keyPath: "transform")
animation.fromValue = NSValue(caTransform3D: CATransform3DIdentity)
animation.toValue = NSValue(caTransform3D: CATransform3DMakeScale(0.001, 0.001, 1))
animation.duration = animationDuration
animation.delegate = self
fromVC.view.layer.removeAllAnimations()
fromVC.view.layer.add(animation, forKey: nil)
} else if operation == .push {
// The Push Animation
// ...
}
}
}
extension Animator: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if let context = self.storedContext
{
print("COMPLETE TRANSITION & ANIMATION STOP")
context.completeTransition(!context.transitionWasCancelled)
}
self.storedContext = nil
}
}
That was my animator class. In my MainViewController the only interesting code is this:
class MainViewController: UIViewController, UINavigationControllerDelegate {
let animator = Animator()
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.delegate = self
}
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
animator.operation = operation
return animador
}
func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
if ! animator.interactive {
return nil
}
return animador
}
}
And in my DetailViewController I have this
class DetailViewController: UIViewController {
weak var animator: Animator?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let masterVC = navigationController!.viewControllers.first as? ViewController {
animador = masterVC.animador
}
let pan = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(didPan(recognizer:)))
pan.edges = .left
view.addGestureRecognizer(pan)
}
#objc func didPan(recognizer: UIScreenEdgePanGestureRecognizer)
{
if recognizer.state == .began
{
animador?.interactive = true
self.navigationController?.popViewController(animated: true)
}
animador?.handlePan(recognizer: recognizer)
}

Related

UIPresentationController change size for a moment when another view is on top

I am trying to make a pop over form the bottom of screen using UIPresentationController, so I followed raywenderlich guide here : https://www.raywenderlich.com/139277/uipresentationcontroller-tutorial-getting-started. I did the exact same thing, I only change the size and y position of the frame. The pop up consist of buttons that open the share sheet , but for some reason when I open the sheet then click "save to files", the "shave to files" view shows up and when I hit cancel my pop over goes full screen for a moment then changes to my custom size.
I tried to debug the app and found out that containerViewWillLayoutSubviews() doesn't get called untill the "save to file" view is dismissed. Anyone have an idea on how to solve this. Thank you
this is my code :
main :
final class MainViewController: UIViewController {
// MARK: - Properties
lazy var slideInTransitioningDelegate = SlideInPresentationManager()
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func showPopup(_ sender: Any) {
let controller = storyboard.instantiateViewController(withIdentifier: NSStringFromClass(MyPopUpController.self))
as! MyPopUpController
slideInTransitioningDelegate.direction = .bottom
slideInTransitioningDelegate.disableCompactHeight = true
controller.transitioningDelegate = slideInTransitioningDelegate
controller.modalPresentationStyle = .custom
}
mypopucontroller
final class MyPopUpController: UIViewController {
#IBAction func share(_ sender: Any) {
let activityController = UIActivityViewController(activityItems: ["message"], applicationActivities: nil)
present(activityController, animated: true)
}
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
}
slide in presentation controller :
final class SlideInPresentationController: UIPresentationController {
// MARK: - Properties
fileprivate var dimmingView: UIView!
private var direction: PresentationDirection
override var frameOfPresentedViewInContainerView: CGRect {
var frame: CGRect = .zero
frame.size = size(forChildContentContainer: presentedViewController, withParentContainerSize: containerView!.bounds.size)
switch direction {
case .right:
frame.origin.x = containerView!.frame.width*(1.0/3.0)
case .bottom:
frame.origin.y = containerView!.frame.height*0.5
default:
frame.origin = .zero
}
return frame
}
// MARK: - Initializers
init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, direction: PresentationDirection) {
self.direction = direction
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
setupDimmingView()
}
override func presentationTransitionWillBegin() {
containerView?.insertSubview(dimmingView, at: 0)
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:|[dimmingView]|", options: [], metrics: nil, views: ["dimmingView": dimmingView]))
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|[dimmingView]|", options: [], metrics: nil, views: ["dimmingView": dimmingView]))
guard let coordinator = presentedViewController.transitionCoordinator else {
dimmingView.alpha = 1.0
return
}
coordinator.animate(alongsideTransition: { _ in
self.dimmingView.alpha = 1.0
})
}
override func dismissalTransitionWillBegin() {
guard let coordinator = presentedViewController.transitionCoordinator else {
dimmingView.alpha = 0.0
return
}
coordinator.animate(alongsideTransition: { _ in
self.dimmingView.alpha = 0.0
})
}
override func containerViewWillLayoutSubviews() {
presentedView?.frame = frameOfPresentedViewInContainerView
}
override func size(forChildContentContainer container: UIContentContainer, withParentContainerSize parentSize: CGSize) -> CGSize {
switch direction {
case .left, .right:
return CGSize(width: parentSize.width*(2.0/3.0), height: parentSize.height)
case .bottom, .top:
return CGSize(width: parentSize.width, height: parentSize.height*0.67)
}
}
}
// MARK: - Private
private extension SlideInPresentationController {
func setupDimmingView() {
dimmingView = UIView()
dimmingView.translatesAutoresizingMaskIntoConstraints = false
dimmingView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
dimmingView.alpha = 0.0
let recognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(recognizer:)))
dimmingView.addGestureRecognizer(recognizer)
}
dynamic func handleTap(recognizer: UITapGestureRecognizer) {
presentingViewController.dismiss(animated: true)
}
}
slidein manager :
final class SlideInPresentationManager: NSObject {
// MARK: - Properties
var direction = PresentationDirection.left
var disableCompactHeight = false
}
// MARK: - UIViewControllerTransitioningDelegate
extension SlideInPresentationManager: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
let presentationController = SlideInPresentationController(presentedViewController: presented, presenting: presenting, direction: direction)
presentationController.delegate = self
return presentationController
}
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return SlideInPresentationAnimator(direction: direction, isPresentation: true)
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return SlideInPresentationAnimator(direction: direction, isPresentation: false)
}
}
// MARK: - UIAdaptivePresentationControllerDelegate
extension SlideInPresentationManager: UIAdaptivePresentationControllerDelegate {
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
if traitCollection.verticalSizeClass == .compact && disableCompactHeight {
return .overFullScreen
} else {
return .none
}
}
func presentationController(_ controller: UIPresentationController, viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle) -> UIViewController? {
guard case(.overFullScreen) = style else { return nil }
return UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "RotateViewController")
}
}
slidein animator:
final class SlideInPresentationAnimator: NSObject {
// MARK: - Properties
let direction: PresentationDirection
let isPresentation: Bool
// MARK: - Initializers
init(direction: PresentationDirection, isPresentation: Bool) {
self.direction = direction
self.isPresentation = isPresentation
super.init()
}
}
// MARK: - UIViewControllerAnimatedTransitioning
extension SlideInPresentationAnimator: UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let key = isPresentation ? UITransitionContextViewControllerKey.to : UITransitionContextViewControllerKey.from
let controller = transitionContext.viewController(forKey: key)!
if isPresentation {
transitionContext.containerView.addSubview(controller.view)
}
let presentedFrame = transitionContext.finalFrame(for: controller)
var dismissedFrame = presentedFrame
switch direction {
case .left:
dismissedFrame.origin.x = -presentedFrame.width
case .right:
dismissedFrame.origin.x = transitionContext.containerView.frame.size.width
case .top:
dismissedFrame.origin.y = -presentedFrame.height
case .bottom:
dismissedFrame.origin.y = transitionContext.containerView.frame.size.height
}
let initialFrame = isPresentation ? dismissedFrame : presentedFrame
let finalFrame = isPresentation ? presentedFrame : dismissedFrame
let animationDuration = transitionDuration(using: transitionContext)
controller.view.frame = initialFrame
UIView.animate(withDuration: animationDuration, animations: {
controller.view.frame = finalFrame
}) { finished in
transitionContext.completeTransition(finished)
}
}
}
You can try to subclass UIPresentationController and override var presentedView: UIView? and enforce presentedView's frame.
override var presentedView: UIView? {
super.presentedView?.frame = frameOfPresentedViewInContainerView
return super.presentedView
}
See example: "Custom View Controller Presentation" from Kyle Bashour https://kylebashour.com/posts/custom-view-controller-presentation-tips

Interactive sliding menu goes to completion as soon as pan gesture starts [Swift]

I am trying to implement a sliding menu that can be interactively dismissed by horizontal panning, same as the ones in Uber and Google apps. Everything works as expected except that, as soon as I start panning horizontally, dismiss goes to completion without following my finger. Any suggestion of where the problem may lie is very appreciated.
I subclassed UIPresentationController to define the presented width of my menu controller. I have custom presentation animator and dismiss animator, and a UIViewControllerTransitioningDelegate object to return them all to UIKit. I also implemented gestureRecognizerShouldBegin(_ gestureRecognizer:) method in my menu controller to allow vertical scrolling.
SlideDismissAnimator
class SlideDismissAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let interactionController: SlideInteractionController?
init(interactionController: SlideInteractionController?) {
self.interactionController = interactionController
super.init()
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.2
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let fromCV = transitionContext.viewController(forKey: .from)!
let initialFrame = transitionContext.finalFrame(for: fromCV)
var finalFrame = initialFrame
finalFrame.origin.x = transitionContext.containerView.frame.width // My menu slides in from right
let duration = transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration, delay: 0, options: .curveEaseOut, animations: {
fromCV.view.frame = finalFrame
}) { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
SlideInteractionController
class SlideInteractionController: UIPercentDrivenInteractiveTransition {
var interactionInProgress = false
private var shouldCompleteTransition = false
private weak var collectionViewController: UICollectionViewController!
init(collectionViewController: UICollectionViewController) {
super.init()
self.collectionViewController = collectionViewController
if let menuController = collectionViewController as? MenuController {
let gesture = UIPanGestureRecognizer(target: self, action: #selector(handleGesture))
menuController.collectionView?.addGestureRecognizer(gesture)
gesture.delegate = menuController
}
}
#objc func handleGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
let translation = gestureRecognizer.translation(in: gestureRecognizer.view!.superview!)
var progress = (translation.x / 100)
progress = CGFloat(fminf(fmaxf(Float(progress), 0.0), 1.0))
switch gestureRecognizer.state {
case .began:
interactionInProgress = true
collectionViewController.dismiss(animated: true, completion: nil)
case .changed:
shouldCompleteTransition = progress > 0.5
update(progress)
case .cancelled:
interactionInProgress = false
cancel()
case .ended:
interactionInProgress = false
if shouldCompleteTransition {
finish()
} else {
cancel()
}
default:
break
}
}
}
MenuController
class MenuController: UICollectionViewController, UIGestureRecognizerDelegate {
var slideInteractionController: SlideInteractionController?
override func viewDidLoad() {
super.viewDidLoad()
setupView()
slideInteractionController = SlideInteractionController(collectionViewController: self)
}
...
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer {
let translation = panGestureRecognizer.translation(in: collectionView)
if translation.x > fabs(translation.y) {
return true
}
}
return false
}
}
I made a sample Project that Use a tableView inside a View that acts as A side Menu drawer Presented over Current Context
my pan Handler
//MARK: Pan gesture Handler
#objc func handlePanGesture(panGesture: UIPanGestureRecognizer)
{
///Get the changes
let translation = panGesture.translation(in: self.view)
///Make View move to left side of Frame
if CGFloat(round(Double((panGesture.view?.frame.origin.x)!))) <= 0
{
panGesture.view!.center = CGPoint(x: panGesture.view!.center.x + translation.x, y: panGesture.view!.center.y)
panGesture.setTranslation(CGPoint.zero, in: self.view)
}
///Do not let View go beyond origin as 0
if CGFloat(round(Double((panGesture.view?.frame.origin.x)!))) > 0
{
panGesture.view?.frame.origin.x = 0
panGesture.setTranslation(CGPoint.zero, in: self.view)
}
///States When Dragging
switch panGesture.state
{
case .changed:
self.setAlphaOfBlurView(origin: (panGesture.view?.frame.maxX)!)
case .ended:
if CGFloat(round(Double((panGesture.view?.frame.maxX)!))) >= self.view.frame.size.width*0.35
{
UIView.animate(withDuration: 0.7, animations: {
panGesture.view?.frame.origin.x = 0
panGesture.setTranslation(CGPoint.zero, in: self.view)
})
}
else
{
UIView.animate(withDuration: 0.4, animations: {
panGesture.view?.frame.origin.x -= self.maximum_x
panGesture.setTranslation(CGPoint.zero, in: self.view)
}, completion: { (success) in
if (success)
{
self.remove(asChildViewController: self.sideMenuVCObject, baseView: self.baseView)
self.baseView.removeFromSuperview()
self.blurView.removeFromSuperview()
//Remove Notification observer
NotificationCenter.default.removeObserver(self,name: NSNotification.Name(rawValue: "hideMenu"),object: nil)
}
})
}
break
default:
print("Default Case")
}
}
Repository Link at GiHub
https://github.com/RockinGarg/Slide-Menu-Drawer.git
Working Video :
https://drive.google.com/open?id=13Q-bBkVlAX7uEweDyQGvNct-dXkBSveT

Swift : UIPercentDrivenInteractiveTransition on cancel?

This is my first iOS development and so I am using this tiny project to learn how the system works and how the language (swift) works too.
I am trying to make a drawer menu similar to android app and a certain number of iOS app.
I found this tutorial that explains well how to do it and how it works : here
Now since I am using a NavigationController with show I have to modify the way it is done.
I swapped the UIViewControllerTransitioningDelegate to a UINavigationControllerDelegate so I can override the navigationController function.
This means I can get the drawer out and dismiss it. It works well with a button or with the gesture.
My problem is the following : If I don't finish to drag the drawer far enough for it to reach the threshold and finishing the animation, it will be cancel and hidden. This is all well and good but when that happens there is no call to a dismiss function meaning that the snapshot I put in place in the PresentMenuAnimator is still in front of all the layers and I am stuck there even though I can interact with what's behind it.
How can I catch a dismiss or a cancel with the NavigationController ? Is that possible ?
Interactor :
import UIKit
class Interactor:UIPercentDrivenInteractiveTransition {
var hasStarted: Bool = false;
var shouldFinish: Bool = false;
}
MenuHelper :
import Foundation
import UIKit
enum Direction {
case Up
case Down
case Left
case Right
}
struct MenuHelper {
static let menuWith:CGFloat = 0.8;
static let percentThreshold:CGFloat = 0.6;
static let snapshotNumber = 12345;
static func calculateProgress(translationInView:CGPoint, viewBounds:CGRect, direction: Direction) -> CGFloat {
let pointOnAxis:CGFloat;
let axisLength:CGFloat;
switch direction {
case .Up, .Down :
pointOnAxis = translationInView.y;
axisLength = viewBounds.height;
case .Left, .Right :
pointOnAxis = translationInView.x;
axisLength = viewBounds.width;
}
let movementOnAxis = pointOnAxis/axisLength;
let positiveMovementOnAxis:Float;
let positiveMovementOnAxisPercent:Float;
switch direction {
case .Right, .Down:
positiveMovementOnAxis = fmaxf(Float(movementOnAxis), 0.0);
positiveMovementOnAxisPercent = fminf(positiveMovementOnAxis, 1.0);
return CGFloat(positiveMovementOnAxisPercent);
case .Left, .Up :
positiveMovementOnAxis = fminf(Float(movementOnAxis), 0.0);
positiveMovementOnAxisPercent = fmaxf(positiveMovementOnAxis, -1.0);
return CGFloat(-positiveMovementOnAxisPercent);
}
}
static func mapGestureStateToInteractor(gestureState:UIGestureRecognizerState, progress:CGFloat, interactor: Interactor?, triggerSegue: () -> Void ) {
guard let interactor = interactor else {return };
switch gestureState {
case .began :
interactor.hasStarted = true;
interactor.shouldFinish = false;
triggerSegue();
case .changed :
interactor.shouldFinish = progress > percentThreshold;
interactor.update(progress);
case .cancelled :
interactor.hasStarted = false;
interactor.shouldFinish = false;
interactor.cancel();
case .ended :
interactor.hasStarted = false;
interactor.shouldFinish
? interactor.finish()
: interactor.cancel();
interactor.shouldFinish = false;
default :
break;
}
}
}
MenuNavigationController :
import Foundation
import UIKit
class MenuNavigationController: UINavigationController, UINavigationControllerDelegate {
let interactor = Interactor()
override func viewDidLoad() {
super.viewDidLoad();
self.delegate = self;
}
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if((toVC as? MenuViewController) != nil) {
return PresentMenuAnimator();
}
else {
return DismissMenuAnimator();
}
}
func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactor.hasStarted ? interactor : nil;
}
}
PresentMenuAnimator :
import UIKit
class PresentMenuAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.6;
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
else {return};
let containerView = transitionContext.containerView;
containerView.insertSubview(toVC.view, aboveSubview: fromVC.view);
let snapshot = fromVC.view.snapshotView(afterScreenUpdates: false);
snapshot?.tag = MenuHelper.snapshotNumber;
snapshot?.isUserInteractionEnabled = false;
snapshot?.layer.shadowOpacity = 0.7;
containerView.insertSubview(snapshot!, aboveSubview: toVC.view);
fromVC.view.isHidden = true;
UIView.animate(withDuration: transitionDuration(using: transitionContext),
animations: {snapshot?.center.x+=UIScreen.main.bounds.width*MenuHelper.menuWith;},
completion: {_ in
fromVC.view.isHidden = false;
transitionContext.completeTransition(!transitionContext.transitionWasCancelled);}
);
}
}
DismissMenuAnimator :
import UIKit
class DismissMenuAnimator : NSObject {
}
extension DismissMenuAnimator : UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.6;
}
func animateTransition(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 snapshot = containerView.viewWithTag(MenuHelper.snapshotNumber)
UIView.animate(withDuration: transitionDuration(using: transitionContext),
animations: {
snapshot?.frame = CGRect(origin: CGPoint.zero, size: UIScreen.main.bounds.size)
},
completion: { _ in
let didTransitionComplete = !transitionContext.transitionWasCancelled
if didTransitionComplete {
containerView.insertSubview(toVC.view, aboveSubview: fromVC.view)
snapshot?.removeFromSuperview()
}
transitionContext.completeTransition(didTransitionComplete)
}
)
}
}
It is possible to know whether the animation was cancelled, and it can be caught in the func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) method from UINavigationControllerDelegate.
Here's a snippet of code on how to do so:
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
navigationController.transitionCoordinator?.notifyWhenInteractionEnds { context in
if context.isCancelled {
// The interactive back transition was cancelled
}
}
}
This method could be put in your MenuNavigationController, in which you could persist your PresentMenuAnimator and tell it that the transition was cancelled, and in there remove the snapshot that's hanging around.
To fix the problem I added a verification in PresentMenuAnimator to check if it the animation was canceled.
If it was then remove the snapshot in the UIView.Animate.

Why isn't my UIPercentDrivenInteractiveTransition working?

I made a custom UIViewControllerAnimatedTransitioning for the dismissal of my detail view controller as so:
class DismissAnimator : NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.4
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
guard let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)?.childViewControllers.first as? MainController,
let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) as? DetailViewController
else {
return
}
containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
fromVC.view.isHidden = true
let snapshot = fromVC.view.snapshotView(afterScreenUpdates: false)
containerView.insertSubview(snapshot!, aboveSubview: toVC.view)
UIView.animate(
withDuration: transitionDuration(using: transitionContext),
animations: {
snapshot!.center.y += UIScreen.main.bounds.height
},
completion: { _ in
fromVC.view.isHidden = false
snapshot?.removeFromSuperview()
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
My MainViewController has the following functions:
extension MainController: UIViewControllerTransitioningDelegate {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "openDetailView" {
let cell = sender as! PopularCell
let indexPath = popularCollectionView.indexPath(for: cell)
let destinationViewController = segue.destination as! DetailViewController
destinationViewController.transitioningDelegate = self
destinationViewController.event = events[indexPath!.row]
destinationViewController.interactor = interactor
}
}
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return OpeningAnimator()
}
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return DismissAnimator()
}
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactor.hasStarted ? interactor : nil
}
and this is how my pan interaction is being handled:
#IBAction func handleGesture(sender: UIPanGestureRecognizer) {
let percentThreshold:CGFloat = 0.3
let translation = sender.translation(in: view)
let progress = progressAlongAxis(pointOnAxis: translation.y, axisLength: view.bounds.height)
guard let interactor = interactor,
let originView = sender.view else { return }
switch originView {
case view:
break
case tableView:
if tableView.contentOffset.y > 0 {
return
}
default:
break
}
switch sender.state {
case .began:
interactor.hasStarted = true
dismiss(animated: true, completion: nil)
case .changed:
interactor.shouldFinish = progress > percentThreshold
interactor.update(progress)
case .cancelled:
interactor.hasStarted = false
interactor.cancel()
case .ended:
interactor.hasStarted = false
interactor.shouldFinish
? interactor.finish()
: interactor.cancel()
default:
break
}
}
Unfortunately, as soon as I barely drag the modal view controller, it disappears automatically, with no interactivity. Putting a breakpoint in the handleGesture(sender:) shows that the function is being called so I'm confused as to what I should do now.
Any help?

Interactive Transition With Push Segue Not Working (Swift)

I'm lost in the universe of the transitions. I want an interactive transition with a push segue. The following code works with a modal segue, but not with a push one :
(With a push segue, the animation is not interactive and is reversed)
FirstViewController.swift
let transitionManager = TransitionManager()
override func viewDidLoad() {
super.viewDidLoad()
transitionManager.sourceViewController = self
// Do any additional setup after loading the view.
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
let dest = segue.destinationViewController as UIViewController
dest.transitioningDelegate = transitionManager
transitionManager.destViewController = dest
}
TransitionManager.swift
class TransitionManager: UIPercentDrivenInteractiveTransition,UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate,UIViewControllerInteractiveTransitioning {
var interactive = false
var presenting = false
var panGesture : UIPanGestureRecognizer!
var destViewController : UIViewController!
var sourceViewController : UIViewController! {
didSet {
panGesture = UIPanGestureRecognizer(target: self, action: "gestureHandler:")
sourceViewController.view.addGestureRecognizer(panGesture)
}
}
func gestureHandler(pan : UIPanGestureRecognizer) {
let translation = pan.translationInView(pan.view!)
let velocity = pan.velocityInView(pan.view!)
let d = translation.x / pan.view!.bounds.width * 0.5
switch pan.state {
case UIGestureRecognizerState.Began :
interactive = true
sourceViewController.performSegueWithIdentifier("1to2", sender: self)
case UIGestureRecognizerState.Changed :
self.updateInteractiveTransition(d)
default :
interactive = false
if d > 0.2 || velocity.x > 0 {
self.finishInteractiveTransition()
}
else {
self.cancelInteractiveTransition()
}
}
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
// create a tuple of our screens
let screens : (from:UIViewController, to:UIViewController) = (transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!, transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!)
let container = transitionContext.containerView()
let toView = screens.to.view
let fromView = screens.from.view
toView.frame = CGRectMake(-320, 0, container.frame.size.width, container.frame.size.height)
container.addSubview(toView)
container.addSubview(fromView)
let duration = self.transitionDuration(transitionContext)
// perform the animation!
UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.8, options: nil, animations: {
toView.frame.origin = container.frame.origin
fromView.frame.origin = CGPointMake(320, 0)
}, completion: { finished in
if(transitionContext.transitionWasCancelled()){
transitionContext.completeTransition(false)
UIApplication.sharedApplication().keyWindow.addSubview(screens.from.view)
}
else {
transitionContext.completeTransition(true)
UIApplication.sharedApplication().keyWindow.addSubview(screens.to.view)
}
})
}
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
return 1
}
// MARK: UIViewControllerTransitioningDelegate protocol methods
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.presenting = true
return self
}
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.presenting = false
return self
}
func interactionControllerForPresentation(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return self.interactive ? self : nil
}
func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return self.interactive ? self : nil
}
}
Storyboard
The segue is from the FirstViewController to the SecondViewController.
Identifier : "1to2"
Segue : Push
Destination : Current
Thanks for your help

Resources