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
}
Related
I am trying to implement a custom drag-to-dismiss interactive transition, using UIViewControllerAnimatedTransitioning and UIPercentDrivenInteractiveTransition.
Here is the animator code for dismissing, it just moves the view down completely out from the frame:
final class SheetDismissAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
0.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromVC = transitionContext.viewController(forKey: .from) else { return }
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
fromVC.view.frame.origin.y = transitionContext.containerView.frame.height
}, completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
And here is the code for the modal view controller I am trying to dismiss, which has the pan gesture recognizer attached to it:
final class ModalViewController: UIViewController {
private let customTransitionDelegate = SheetTransitioningDelegate()
private var interactionController: UIPercentDrivenInteractiveTransition?
init() {
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .custom
transitioningDelegate = customTransitionDelegate
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red
view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:))))
}
#objc func handleGesture(_ gesture: UIPanGestureRecognizer) {
let translate = gesture.translation(in: view)
let percent = translate.y / view.bounds.height
switch gesture.state {
case .began:
interactionController = UIPercentDrivenInteractiveTransition()
customTransitionDelegate.interactionController = interactionController
dismiss(animated: true)
case .changed:
interactionController?.update(percent)
case .cancelled:
interactionController?.cancel()
case .ended:
let velocity = gesture.velocity(in: gesture.view)
if percent > 0.5 || velocity.y > 700 {
interactionController?.finish()
} else {
interactionController?.cancel()
}
interactionController = nil
default:
break
}
}
}
The relevant part I believe is the handleGesture(_ gesture: UIPanGestureRecognizer) function, which calculates the percentage for advancing the animation. Pasted the whole code for clarity.
I would expect the animation to follow the pan gesture linearly, but this is what happens instead:
I solved the issue by switching to UIViewPropertyAnimator:
final class SheetDismissAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
0.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let propertyAnimator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext),
timingParameters: UISpringTimingParameters(dampingRatio: 1))
propertyAnimator.addAnimations {
switch self.type {
case .presented:
guard let toVC = transitionContext.viewController(forKey: .to) else { break }
toVC.view.frame = transitionContext.finalFrame(for: toVC)
case .dismissed:
transitionContext.view(forKey: .from)?.frame.origin.y = transitionContext.containerView.frame.maxY
}
}
propertyAnimator.addCompletion { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
propertyAnimator.startAnimation()
}
}
I also added the following into my gesture handling code, which is a basic attempt at transfering the gesture's velocity to the animation speed.
case .ended:
if percent > 0.45 || velocity.y > 700 {
interactor.timingCurve = UICubicTimingParameters(animationCurve: .linear)
interactor.completionSpeed = max(1, (view.frame.height * (1 / interactor.duration) / velocity.y))
interactor.finish()
} else {
interactor.cancel()
}
isInteractive = false
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.
Introduction
I'm creating an app that has, in its rootViewController, a UITableView and a UIPanGestureRecognizer attached to a small UIView acting as a "handle" which enables a custom view controller transition for a UIViewController called "SlideOutViewController" to be panned from the right.
Issue
I have noticed two issues with my approach. But the actual custom transition works as expected.
When the SlideOutViewController is created it is not attached to the navigation stack I believe, therefore it has no associated navigationBar. And if I use the navigationController to push it on the stack, I loose the interactive transition.
Side note: I have not found a way to connect the handle to the SlideOutViewController that is interactively dragged out. So the translation of the handle is not consistent with the SlideOutViewControllers position.
Question
How can I add the SlideOutViewController to the navigation stack? So that the SlideOutViewController transitions with a navigationBar when I trigger the UIPanGestureRecognizer?
My code
In the rootViewController.
class RootViewController: UIViewController {
...
let slideControllerHandle = UIView()
var interactionController : UIPercentDrivenInteractiveTransition?
override func viewDidLoad() {
super.viewDidLoad()
... // Setting up the table view etc...
setupPanGForSlideOutController()
}
private func setupPanGForSlideOutController() {
slideControllerHandle.translatesAutoresizingMaskIntoConstraints = false
slideControllerHandle.layer.borderColor = UIColor.black.cgColor
slideControllerHandle.layer.borderWidth = 1
slideControllerHandle.layer.cornerRadius = 30
view.addSubview(slideControllerHandle)
slideControllerHandle.frame = CGRect(x: view.frame.width - 12.5, y: view.frame.height / 2, width: 25, height: 60)
let panGestureForCalendar = UIPanGestureRecognizer(target: self, action: #selector(handlePanGestureForSlideOutViewController(_:)))
slideControllerHandle.addGestureRecognizer(panGestureForCalendar)
}
#objc private func handlePanGestureForSlideOutViewController(_ gesture: UIPanGestureRecognizer) {
let xPosition = gesture.location(in: view).x
let percent = 1 - (xPosition / view.frame.size.width)
switch gesture.state {
case .began:
guard let slideOutController = storyboard?.instantiateViewController(withIdentifier: "CNSlideOutViewControllerID") as? SlideOutViewController else { fatalError("Sigh...") }
interactionController = UIPercentDrivenInteractiveTransition()
slideOutController.customTransitionDelegate.interactionController = interactionController
self.present(slideOutController, animated: true)
case .changed:
slideControllerHandle.center = CGPoint(x: xPosition, y: slideControllerHandle.center.y)
interactionController?.update(percent)
case .ended, .cancelled:
let velocity = gesture.velocity(in: view)
interactionController?.completionSpeed = 0.999
if percent > 0.5 || velocity.x < 10 {
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.5, options: .curveEaseInOut, animations: {
self.slideControllerHandle.center = CGPoint(x: self.view.frame.width, y: self.slideControllerHandle.center.y)
})
interactionController?.finish()
} else {
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.5, options: .curveEaseInOut, animations: {
self.slideControllerHandle.center = CGPoint(x: -25, y: self.slideControllerHandle.center.y)
})
interactionController?.cancel()
}
interactionController = nil
default:
break
}
}
The SlideOutViewController
class SlideOutViewController: UIViewController {
var interactionController : UIPercentDrivenInteractiveTransition?
let customTransitionDelegate = TransitionDelegate()
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
modalPresentationStyle = .custom
transitioningDelegate = customTransitionDelegate
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red
navigationItem.title = "Slide Controller"
let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addNewData(_:)))
navigationItem.setRightBarButton(addButton, animated: true)
}
}
The custom transition code. Based on Rob's descriptive answer on this SO question
TransitionDelegate
class TransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
weak var interactionController : UIPercentDrivenInteractiveTransition?
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CNRightDragAnimationController(transitionType: .presenting)
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CNRightDragAnimationController(transitionType: .dismissing)
}
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return PresentationController(presentedViewController: presented, presenting: presenting)
}
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactionController
}
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactionController
}
}
DragAnimatedTransitioning
class CNRightDragAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
enum TransitionType {
case presenting
case dismissing
}
let transitionType: TransitionType
init(transitionType: TransitionType) {
self.transitionType = transitionType
super.init()
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let inView = transitionContext.containerView
let toView = transitionContext.view(forKey: .to)!
let fromView = transitionContext.view(forKey: .from)!
var frame = inView.bounds
switch transitionType {
case .presenting:
frame.origin.x = frame.size.width
toView.frame = frame
inView.addSubview(toView)
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
toView.frame = inView.bounds
}, completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
case .dismissing:
toView.frame = frame
inView.insertSubview(toView, belowSubview: fromView)
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
frame.origin.x = frame.size.width
fromView.frame = frame
}, completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}
}
PresentationController
class PresentationController: UIPresentationController {
override var shouldRemovePresentersView: Bool { return true }
}
Thanks for reading my question.
The animation code you’ve taken this from is for custom “present” (e.g. modal) transitions. But if you want a custom navigation as you push/pop when using a navigation controller, you specify a delegate for your UINavigationController and then return the appropriate transitioning delegate in navigationController(_:animationControllerFor:from:to:). And also implement navigationController(_:interactionControllerFor:) and return your interaction controller there.
E.g. I'd do something like:
class FirstViewController: UIViewController {
let navigationDelegate = CustomNavigationDelegate()
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.delegate = navigationDelegate
navigationDelegate.addPushInteractionController(to: view)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
navigationDelegate.pushDestination = { [weak self] in
self?.storyboard?.instantiateViewController(withIdentifier: "Second")
}
}
}
Where:
class CustomNavigationDelegate: NSObject, UINavigationControllerDelegate {
var interactionController: UIPercentDrivenInteractiveTransition?
var current: UIViewController?
var pushDestination: (() -> UIViewController?)?
func navigationController(_ navigationController: UINavigationController,
animationControllerFor operation: UINavigationController.Operation,
from fromVC: UIViewController,
to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CustomNavigationAnimator(transitionType: operation)
}
func navigationController(_ navigationController: UINavigationController,
interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactionController
}
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
current = viewController
}
}
// MARK: - Push
extension CustomNavigationDelegate {
func addPushInteractionController(to view: UIView) {
let swipe = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handlePushGesture(_:)))
swipe.edges = [.right]
view.addGestureRecognizer(swipe)
}
#objc private func handlePushGesture(_ gesture: UIScreenEdgePanGestureRecognizer) {
guard let pushDestination = pushDestination else { return }
let position = gesture.translation(in: gesture.view)
let percentComplete = min(-position.x / gesture.view!.bounds.width, 1.0)
switch gesture.state {
case .began:
interactionController = UIPercentDrivenInteractiveTransition()
guard let controller = pushDestination() else { fatalError("No push destination") }
current?.navigationController?.pushViewController(controller, animated: true)
case .changed:
interactionController?.update(percentComplete)
case .ended, .cancelled:
let speed = gesture.velocity(in: gesture.view)
if speed.x < 0 || (speed.x == 0 && percentComplete > 0.5) {
interactionController?.finish()
} else {
interactionController?.cancel()
}
interactionController = nil
default:
break
}
}
}
// MARK: - Pop
extension CustomNavigationDelegate {
func addPopInteractionController(to view: UIView) {
let swipe = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handlePopGesture(_:)))
swipe.edges = [.left]
view.addGestureRecognizer(swipe)
}
#objc private func handlePopGesture(_ gesture: UIScreenEdgePanGestureRecognizer) {
let position = gesture.translation(in: gesture.view)
let percentComplete = min(position.x / gesture.view!.bounds.width, 1)
switch gesture.state {
case .began:
interactionController = UIPercentDrivenInteractiveTransition()
current?.navigationController?.popViewController(animated: true)
case .changed:
interactionController?.update(percentComplete)
case .ended, .cancelled:
let speed = gesture.velocity(in: gesture.view)
if speed.x > 0 || (speed.x == 0 && percentComplete > 0.5) {
interactionController?.finish()
} else {
interactionController?.cancel()
}
interactionController = nil
default:
break
}
}
}
And
class CustomNavigationAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let transitionType: UINavigationController.Operation
init(transitionType: UINavigationController.Operation) {
self.transitionType = transitionType
super.init()
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let inView = transitionContext.containerView
let toView = transitionContext.view(forKey: .to)!
let fromView = transitionContext.view(forKey: .from)!
var frame = inView.bounds
switch transitionType {
case .push:
frame.origin.x = frame.size.width
toView.frame = frame
inView.addSubview(toView)
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
toView.frame = inView.bounds
}, completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
case .pop:
toView.frame = frame
inView.insertSubview(toView, belowSubview: fromView)
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
frame.origin.x = frame.size.width
fromView.frame = frame
}, completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
case .none:
break
}
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}
}
Then, if the second view controller wanted to have the custom interactive pop plus the ability to swipe to the third view controller:
class SecondViewController: UIViewController {
var navigationDelegate: CustomNavigationDelegate { return navigationController!.delegate as! CustomNavigationDelegate }
override func viewDidLoad() {
super.viewDidLoad()
navigationDelegate.addPushInteractionController(to: view)
navigationDelegate.addPopInteractionController(to: view)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
navigationDelegate.pushDestination = { [weak self] in
self?.storyboard?.instantiateViewController(withIdentifier: "Third")
}
}
}
But if the last view controller can't push to anything, but only pop:
class ThirdViewController: UIViewController {
var navigationDelegate: CustomNavigationDelegate { return navigationController!.delegate as! CustomNavigationDelegate }
override func viewDidLoad() {
super.viewDidLoad()
navigationDelegate.addPopInteractionController(to: view)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
navigationDelegate.pushDestination = nil
}
}
We have to implement UIViewController that supports all interface orientations and may be dismissed by swipe-down gesture.
But presenting UIViewController supports only portrait orientation.
extension TransitioningDelegate: UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toView = transitionContext.view(forKey: .to), let fromView = transitionContext.view(forKey: .from) else {
return
}
let containerView = transitionContext.containerView
let containerFrame = containerView.frame
let targetPoint = CGPoint(x: containerFrame.minX, y: containerFrame.maxY).applying(fromView.transform)
toView.frame = containerView.bounds
containerView.insertSubview(toView, belowSubview: fromView)
UIView.animate(withDuration: self.transitionDuration(using: transitionContext),
animations: {
fromView.frame.origin = targetPoint
},
completion: { (finished) in
transitionContext.completeTransition(finished && !transitionContext.transitionWasCancelled)
})
}
}
#objc func handlePan(_ sender: UIPanGestureRecognizer) {
guard let mainView = sender.view else { return }
let translation = max(0, sender.translation(in: mainView).y)
let percent = translation/mainView.bounds.height
switch sender.state {
case .began:
self.hasStarted = true
self.presentedViewController?.dismiss(animated: true, completion: {
print("COMPLETION")
})
case .changed:
self.interactor.update(percent)
case .cancelled, .failed:
self.hasStarted = false
self.interactor.cancel()
case .ended:
self.hasStarted = false
if percent > 0.3 {
self.interactor.finish()
} else {
self.interactor.cancel()
}
default:
break
}
}
When pan gesture happens, layout of presented UIViewController becomes invalid.
Presented UIViewController changes its orientation,
and pan gesture not being handled anymore.
COMPLETED PROJECT
Try this code in viewWillappear
super.viewWillAppear(true)
self.view.frame = UIScreen.main.bounds
self.view.layoutIfNeeded()
I have a TabBarController that has 3 navigationController, where each has a corresponding viewController: AViewController, BViewController and CViewController. First one has a UIView element that I want to run animation against it when viewDisappears using:
UIView.animateWithDuration(duration, delay: 0.0, options: .CurveEaseOut, animations: {
during...
override func viewWillDisappear(animated: Bool) {
If user clicks on item 2 or 3 in the tabbar, I want that animation to take place first and then take the user to the item #2 or #3.
Problem is that that when user clicks a different item, this code in the TabBarController is triggered first before the viewwillDisappears in my ViewController A
override func tabBar(tabBar: UITabBar, didSelectItem item: UITabBarItem) {
and then my animation doesn't run.
Where should I place my animation so it runs before user is taken to a different item in the tabbar?
You could implement tabBarController:shouldSelectViewController: from UITabBarControllerDelegate, return false, play your animation and then programatically change tab.
You need to write your own UIViewControllerAnimatedTransitioning (https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewControllerAnimatedTransitioning_Protocol/)
try this code:
TabBarController.swift
import UIKit
class TabBarController: UITabBarController, UITabBarControllerDelegate {
var buttons = [UIButton]()
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
var newViewControllers = [UIViewController]()
for index in 0..<2 {
let viewController = ViewController()
var tabBarItem = UITabBarItem()
switch index {
case 0:
viewController.view.backgroundColor = UIColor.blueColor()
viewController.label.textColor = UIColor.whiteColor()
tabBarItem = UITabBarItem(tabBarSystemItem: .More, tag: index)
case 1:
viewController.view.backgroundColor = UIColor.greenColor()
viewController.label.textColor = UIColor.blackColor()
tabBarItem = UITabBarItem(tabBarSystemItem: .Favorites, tag: index)
default:
break
}
viewController.label.text = "Text \(index)"
viewController.tabBarItem = tabBarItem
newViewControllers.append(viewController)
//tabBar.items![index] = tabBarItem
}
selectedIndex = 0
// = newViewControllers
setViewControllers(newViewControllers, animated: false)
// Do any additional setup after loading the view, typically from a nib.
}
func tabBarController(tabBarController: UITabBarController, animationControllerForTransitionFromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let fromIndex = tabBarController.viewControllers!.indexOf(fromVC)!
let toIndex = tabBarController.viewControllers!.indexOf(toVC)!
let tabChangeDirection: TabOperationDirection = toIndex < fromIndex ? .Left : .Right
let transitionType = SDETransitionType.TabTransition(tabChangeDirection)
let slideAnimationController = SlideAnimationController(type: transitionType)
return slideAnimationController
}
}
ViewController.swift
import UIKit
class ViewController: UIViewController {
let label = UILabel(frame: CGRect(x: 40, y: 60, width: 80, height: 40))
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(label)
}
var labelHidden = false {
willSet(value) {
if value {
self.label.frame.origin.y = -40
} else {
self.label.frame.origin.y = 60
}
}
}
}
SlideAnimationController.swift (resource: https://github.com/seedante/iOS-ViewController-Transition-Demo)
import UIKit
enum SDETransitionType{
case NavigationTransition(UINavigationControllerOperation)
case TabTransition(TabOperationDirection)
case ModalTransition(ModalOperation)
}
enum TabOperationDirection{
case Left, Right
}
enum ModalOperation{
case Presentation, Dismissal
}
class SlideAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
private var transitionType: SDETransitionType
init(type: SDETransitionType) {
transitionType = type
super.init()
}
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.3
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
guard let containerView = transitionContext.containerView(), fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey), toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) else{
return
}
let fromView = fromVC.view
let toView = toVC.view
var translation = containerView.frame.width
var toViewTransform = CGAffineTransformIdentity
var fromViewTransform = CGAffineTransformIdentity
switch transitionType{
case .NavigationTransition(let operation):
translation = operation == .Push ? translation : -translation
toViewTransform = CGAffineTransformMakeTranslation(translation, 0)
fromViewTransform = CGAffineTransformMakeTranslation(-translation, 0)
case .TabTransition(let direction):
translation = direction == .Left ? translation : -translation
fromViewTransform = CGAffineTransformMakeTranslation(translation, 0)
toViewTransform = CGAffineTransformMakeTranslation(-translation, 0)
case .ModalTransition(let operation):
translation = containerView.frame.height
toViewTransform = CGAffineTransformMakeTranslation(0, (operation == .Presentation ? translation : 0))
fromViewTransform = CGAffineTransformMakeTranslation(0, (operation == .Presentation ? 0 : translation))
}
switch transitionType{
case .ModalTransition(let operation):
switch operation{
case .Presentation: containerView.addSubview(toView)
case .Dismissal: break
}
default: containerView.addSubview(toView)
}
toView.transform = toViewTransform
if let fromVC = fromVC as? ViewController {
UIView.animateWithDuration(0.3, delay: 0.0, options: .CurveEaseOut, animations: {
fromVC.labelHidden = true
}, completion: {
bool in
UIView.animateWithDuration(self.transitionDuration(transitionContext), animations: {
fromView.transform = fromViewTransform
toView.transform = CGAffineTransformIdentity
}, completion: { finished in
fromView.transform = CGAffineTransformIdentity
toView.transform = CGAffineTransformIdentity
let isCancelled = transitionContext.transitionWasCancelled()
transitionContext.completeTransition(!isCancelled)
fromVC.labelHidden = false
})
})
}
}
}