I am trying to achieve some thing like following side menu open from tabbar item click.
I used the following class for Transition Animation ...
class SlideInTransition: NSObject, UIViewControllerAnimatedTransitioning {
var isPresenting = false
let dimmingView = UIView()
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 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.3
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)
}
}
}
and use it in my code as follow
guard let menuViewController = storyboard?.instantiateViewController(withIdentifier: "MenuVC") as? MenuVC else { return }
menuViewController.modalPresentationStyle = .overCurrentContext
menuViewController.transitioningDelegate = self as? UIViewControllerTransitioningDelegate
menuViewController.tabBarItem.image = UIImage(named: "ico_menu")
menuViewController.tabBarItem.selectedImage = UIImage(named: "ico_menu")
viewControllers = [orderVC,serverdVC,canceledVC,menuViewController]
extension TabbarVC: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transiton.isPresenting = true
return transiton
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transiton.isPresenting = false
return transiton
}
}
but animation doe't work at all ... I want to open it like side menu over current context ..
How can i achieve some thing like that ...
TabBar is not made to handle animate transition for just a single child View controller. If you apply a custom transition, it will be applied in all of its tabs (child view controllers). Plus last time i checked, airbnb's app doesn't behave like that when opening the user profile. :)
What you can do, though, is have a separate menu button at the top of your navigation view controller or wherever and call the slide in from there:
func slideInView() {
let vcToShow = MenuViewController()
vcToShow.modalPresentationStyle = .overCurrentContext
vcToShow.transitioningDelegate = self as? UIViewControllerTransitioningDelegate
present(vcToShow, animated: true, completion: nil)
}
Or if you insist on having the menu a part of the tabs, then you can do this.
Hope this helps. :)
Related
I have a small custom view controller (it's a menu) I'm presenting, and I want you to be able to halt the presentation half way through if you change your mind.
Per this WWDC 2016 video, doing so should be pretty easy and just require the implementation of interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating, and return, say a UIViewPropertyAnimator.
However I'm not finding this to be the case. If I return my property animator there, and programmatically call my view controller to be dismissed half way through the transition, it waits until the animation is fully concluded for the dismiss animation to occur. It won't interrupt.
*Note: this is with a "normal" animation controller, not an interactive one. Per the video this should be fine, it's not inherently an interactive transition"
I set up my corresponding controllers as shown:
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return MyMenuAnimationController(type: .presentation)
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return MyMenuAnimationController(type: .dismissal)
}
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return MyMenuPresentationController(presentedViewController: presented, presenting: presenting)
}
class MyMenuAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
enum AnimationControllerType { case presentation, dismissal }
let type: AnimationControllerType
var animatorForCurrentSession: UIViewPropertyAnimator?
init(type: AnimationControllerType) {
self.type = type
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 5.0 // Artificially long for testing
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
if type == .presentation, let myMenuMenu: MyMenuMenu = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) as? MyMenuMenu {
transitionContext.containerView.addSubview(myMenuMenu.view)
}
interruptibleAnimator(using: transitionContext).startAnimation()
}
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
if let animatorForCurrentSession = animatorForCurrentSession {
return animatorForCurrentSession
}
let propertyAnimator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), dampingRatio: 0.75)
propertyAnimator.isInterruptible = true
propertyAnimator.isUserInteractionEnabled = true
let isPresenting = type == .presentation
guard let myMenuMenu: MyMenuMenu = {
return (isPresenting ? transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) : transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)) as? MyMenuMenu
}() else {
preconditionFailure("Menu should be accessible")
}
myMenuMenu.view.frame = transitionContext.finalFrame(for: myMenuMenu)
let initialAlpha: CGFloat = isPresenting ? 0.0 : 1.0
let finalAlpha: CGFloat = isPresenting ? 1.0 : 0.0
myMenuMenu.view.alpha = initialAlpha
propertyAnimator.addAnimations {
myMenuMenu.view.alpha = finalAlpha
}
propertyAnimator.addCompletion { (position) in
guard position == .end else { return }
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
self.animatorForCurrentSession = nil
}
self.animatorForCurrentSession = propertyAnimator
return propertyAnimator
}
private func calculateTranslationRequired(forMyMenuMenuFrame myMenuMenuFrame: CGRect, toDesiredPoint desiredPoint: CGPoint) -> CGVector {
let centerPointOfMenuView = CGPoint(x: myMenuMenuFrame.origin.x + (myMenuMenuFrame.width / 2.0), y: myMenuMenuFrame.origin.y + (myMenuMenuFrame.height / 2.0))
let translationRequired = CGVector(dx: desiredPoint.x - centerPointOfMenuView.x, dy: desiredPoint.y - centerPointOfMenuView.y)
return translationRequired
}
}
And a simple presentation controller:
class MyMenuPresentationController: UIPresentationController {
override var frameOfPresentedViewInContainerView: CGRect {
return CGRect(x: 100, y: 100, width: 100, height: 100)
}
}
But as I said, if I present it simply and then 1 second into the 5 second animation I dismiss it (signifying someone tapping outside the view controller to dismiss it mid transition), nothing occurs until the animation is done. It's as if it's queued up, but won't execute until the presentation is completed.
let myMenuMenu = MyMenuMenu()
present(myMenuMenu, animated: true, completion: nil)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
myMenuMenu.dismiss(animated: true, completion: nil)
}
Am I misunderstanding a core part of the custom view controller transition flow? Do I have to make it an interactive transition? Something else? In the event that an interactive transition is required, where do I update the interaction controller in my code with progress? With a gesture it's obvious, but this is just a "fade 0 alpha to 1 alpha" animation.
I'm trying to create a transition effect on a UITabBarController somewhat similar to the Facebook app. I managed to get a "scrolling effect" working on tab switch, but I can't seem to figure out how to cross dissolve (or it doesn't work at least).
Here's my current code:
import UIKit
class ScrollingTabBarControllerDelegate: NSObject, UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return ScrollingTransitionAnimator(tabBarController: tabBarController, lastIndex: tabBarController.selectedIndex)
}
}
class ScrollingTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
weak var transitionContext: UIViewControllerContextTransitioning?
var tabBarController: UITabBarController!
var lastIndex = 0
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.2
}
init(tabBarController: UITabBarController, lastIndex: Int) {
self.tabBarController = tabBarController
self.lastIndex = lastIndex
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
self.transitionContext = transitionContext
let containerView = transitionContext.containerView
let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
containerView.addSubview(toViewController!.view)
var viewWidth = toViewController!.view.bounds.width
if tabBarController.selectedIndex < lastIndex {
viewWidth = -viewWidth
}
toViewController!.view.transform = CGAffineTransform(translationX: viewWidth, y: 0)
UIView.animate(withDuration: self.transitionDuration(using: (self.transitionContext)), delay: 0.0, usingSpringWithDamping: 1.2, initialSpringVelocity: 2.5, options: .transitionCrossDissolve, animations: {
toViewController!.view.transform = CGAffineTransform.identity
fromViewController!.view.transform = CGAffineTransform(translationX: -viewWidth, y: 0)
}, completion: { _ in
self.transitionContext?.completeTransition(!self.transitionContext!.transitionWasCancelled)
fromViewController!.view.transform = CGAffineTransform.identity
})
}
}
Would be great if anyone know how to get this to work, been trying for days now without progress... :/
edit: I got a cross dissolve working by replacing the UIView.animate block with:
UIView.transition(with: containerView, duration: 0.2, options: .transitionCrossDissolve, animations: {
toViewController!.view.transform = CGAffineTransform.identity
fromViewController!.view.transform = CGAffineTransform(translationX: -viewWidth, y: 0)
}, completion: { _ in
self.transitionContext?.completeTransition(!self.transitionContext!.transitionWasCancelled)
fromViewController!.view.transform = CGAffineTransform.identity
})
However, the animation is really laggy and not usable :(
edit 2: For people trying to use these snippets, don't forget to hook up the delegate for the UITabBarController, otherwise nothing will happen.
edit 3: I've found a Swift library that does exactly what I was looking for:
https://github.com/Interactive-Studio/TransitionableTab
There is a simpler way to doing this. Add the following code in the tabbar delegate:
Working on Swift 2, 3 and 4
class MySubclassedTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
}
extension MySubclassedTabBarController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
guard let fromView = selectedViewController?.view, let toView = viewController.view else {
return false // Make sure you want this as false
}
if fromView != toView {
UIView.transition(from: fromView, to: toView, duration: 0.3, options: [.transitionCrossDissolve], completion: nil)
}
return true
}
}
EDIT (4/23/18)
Since this answer is getting popular, I updated the code to remove the force unwraps, which is a bad practice, and added the guard statement.
EDIT (7/11/18)
#AlbertoGarcía is right. If you tap the tabbar icon twice you get a blank screen. So I added an extra check
If you want to use UIViewControllerAnimatedTransitioning to do something more custom than UIView.transition, take a look at this gist.
// MyTabController.swift
import UIKit
class MyTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
}
extension MyTabBarController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return MyTransition(viewControllers: tabBarController.viewControllers)
}
}
class MyTransition: NSObject, UIViewControllerAnimatedTransitioning {
let viewControllers: [UIViewController]?
let transitionDuration: Double = 1
init(viewControllers: [UIViewController]?) {
self.viewControllers = viewControllers
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return TimeInterval(transitionDuration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let fromView = fromVC.view,
let fromIndex = getIndex(forViewController: fromVC),
let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to),
let toView = toVC.view,
let toIndex = getIndex(forViewController: toVC)
else {
transitionContext.completeTransition(false)
return
}
let frame = transitionContext.initialFrame(for: fromVC)
var fromFrameEnd = frame
var toFrameStart = frame
fromFrameEnd.origin.x = toIndex > fromIndex ? frame.origin.x - frame.width : frame.origin.x + frame.width
toFrameStart.origin.x = toIndex > fromIndex ? frame.origin.x + frame.width : frame.origin.x - frame.width
toView.frame = toFrameStart
DispatchQueue.main.async {
transitionContext.containerView.addSubview(toView)
UIView.animate(withDuration: self.transitionDuration, animations: {
fromView.frame = fromFrameEnd
toView.frame = frame
}, completion: {success in
fromView.removeFromSuperview()
transitionContext.completeTransition(success)
})
}
}
func getIndex(forViewController vc: UIViewController) -> Int? {
guard let vcs = self.viewControllers else { return nil }
for (index, thisVC) in vcs.enumerated() {
if thisVC == vc { return index }
}
return nil
}
}
I was struggling with the tab bar animation both from a user tap and programmatically calling selectedIndex = X since the accepted solution didn't work for me when setting the selected tab programatically.
In the end I managed to solve it by a UITabBarControllerDelegate and a custom UIViewControllerAnimatedTransitioning as follows:
extension MainController: UITabBarControllerDelegate {
public func tabBarController(
_ tabBarController: UITabBarController,
animationControllerForTransitionFrom fromVC: UIViewController,
to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return FadePushAnimator()
}
}
Where the FadePushAnimator looks like this:
class FadePushAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let toViewController = transitionContext.viewController(forKey: .to)
else {
return
}
transitionContext.containerView.addSubview(toViewController.view)
toViewController.view.alpha = 0
let duration = self.transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration, animations: {
toViewController.view.alpha = 1
}, completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
This approach supports any sort of custom animation and works both on user tap and setting the selected tab programatically. Tested on Swift 5.
To expand on #gmogames answer: https://stackoverflow.com/a/45362914/1993937
I couldn't get this to animate when selecting the tab bar index via code, as calling:
tabBarController.setSeletedIndex(0)
Doesn't seem to go through the same call heirarchy, and it skips the method:
tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController)
entirely, resulting in no animation.
In my code I wanted to have an animation transition for a user tapping a tab bar item in addition to me setting the tab bar item in-code manually under certain circumstances.
Here is my addition to the solution above which adds a different method to set the selected index via code that will animate the transition:
import Foundation
import UIKit
#objc class CustomTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
#objc func set(selectedIndex index : Int) {
_ = self.tabBarController(self, shouldSelect: self.viewControllers![index])
}
}
#objc extension CustomTabBarController: UITabBarControllerDelegate {
#objc func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
guard let fromView = selectedViewController?.view, let toView = viewController.view else {
return false // Make sure you want this as false
}
if fromView != toView {
UIView.transition(from: fromView, to: toView, duration: 0.3, options: [.transitionCrossDissolve], completion: { (true) in
})
self.selectedViewController = viewController
}
return true
}
}
Now just call
tabBarController.setSelectedWithIndex(1)
for an in-code animated transition!
I still think it is unfortunate that to get this done we have to override a method that isn't a setter and manipulate data within it. It doesn't make the tab bar controller as extensible as it should be if this is the method that we need to override to get this done.
So, a few years later and more experienced, after revisiting my own question for the same behaviour, I improved a little bit upon Derek's answer.
I inherited most of his code (as it seems like the best solution).
What I changed
I added a crossDissolve animation (as I originally wanted) to the slide animation by adding a toCoverView and fromCoverView, these are snapshotviews of the other view which will be used to fade in/out at the same time.
Changed the frame width to already start at 75% instead of having to translate the full 100% width, it's only translating 25% now which makes it feel snappier.
Added SpringWithDamping and initialSpringVelocity settings.
These changes made it feel just about as close as I could get it to Facebook's implementation and I'm personally quite happy with it.
Here's the adapted answer (most of the credit goes to Derek so be sure to upvote him):
class MyTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
}
extension MyTabBarController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return MyTransition(viewControllers: tabBarController.viewControllers)
}
}
class MyTransition: NSObject, UIViewControllerAnimatedTransitioning {
let viewControllers: [UIViewController]?
let transitionDuration: Double = 0.2
init(viewControllers: [UIViewController]?) {
self.viewControllers = viewControllers
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return TimeInterval(transitionDuration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let fromView = fromVC.view,
let fromIndex = getIndex(forViewController: fromVC),
let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to),
let toView = toVC.view,
let toIndex = getIndex(forViewController: toVC)
else {
transitionContext.completeTransition(false)
return
}
let frame = transitionContext.initialFrame(for: fromVC)
var fromFrameEnd = frame
var toFrameStart = frame
let quarterFrame = frame.width * 0.25
fromFrameEnd.origin.x = toIndex > fromIndex ? frame.origin.x - quarterFrame : frame.origin.x + quarterFrame
toFrameStart.origin.x = toIndex > fromIndex ? frame.origin.x + quarterFrame : frame.origin.x - quarterFrame
toView.frame = toFrameStart
let toCoverView = fromView.snapshotView(afterScreenUpdates: false)
if let toCoverView = toCoverView {
toView.addSubview(toCoverView)
}
let fromCoverView = toView.snapshotView(afterScreenUpdates: false)
if let fromCoverView = fromCoverView {
fromView.addSubview(fromCoverView)
}
DispatchQueue.main.async {
transitionContext.containerView.addSubview(toView)
UIView.animate(withDuration: self.transitionDuration, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.8, options: [.curveEaseOut], animations: {
fromView.frame = fromFrameEnd
toView.frame = frame
toCoverView?.alpha = 0
fromCoverView?.alpha = 1
}) { (success) in
fromCoverView?.removeFromSuperview()
toCoverView?.removeFromSuperview()
fromView.removeFromSuperview()
transitionContext.completeTransition(success)
}
}
}
func getIndex(forViewController vc: UIViewController) -> Int? {
guard let vcs = self.viewControllers else { return nil }
for (index, thisVC) in vcs.enumerated() {
if thisVC == vc { return index }
}
return nil
}
}
The only thing I've yet to figure out is how to make it "interruptible" like Facebook does. I know there's a interruptibleAnimator function for this but I haven't been able to make it work yet.
I've a custom animation that works correctly except that, at the end of dismiss animation, there is a black screen.
The code of the transition is:
class FolderAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
let duration = 5.0
var presenting = true
var originFrame = CGRect.zero
var selectedFolderCell: FolderCollectionViewCell?
func transitionDuration(_ transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(_ transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView()
let toViewC = transitionContext.viewController(forKey: UITransitionContextToViewControllerKey)!
let fromViewC = transitionContext.viewController(forKey: UITransitionContextFromViewControllerKey)!
let folderViewC = presenting ? fromViewC as! ViewController : transitionContext.viewController(forKey: UITransitionContextToViewControllerKey) as! ViewController
let projectViewC = presenting ? toViewC as! ProjectViewController : transitionContext.viewController(forKey: UITransitionContextFromViewControllerKey) as! ProjectViewController
let cellView = presenting ? (folderViewC.folderCollectionView.cellForItem(at: (folderViewC.folderCollectionView.indexPathsForSelectedItems()?.first!)!) as! FolderCollectionViewCell).folderView : projectViewC.containerView
let cellSnapshot: UIView = presenting ? cellView!.snapshotView(afterScreenUpdates: false)! : cellView!.snapshotView(afterScreenUpdates: false)!
let cellFrame = containerView.convert(cellView!.frame, from: cellView!.superview)
cellSnapshot.frame = cellFrame
cellView!.isHidden = true
toViewC.view.frame = transitionContext.finalFrame(for: toViewC)
toViewC.view.layoutIfNeeded()
toViewC.view.alpha = 0
presenting ? (projectViewC.containerView.isHidden = true) : (self.selectedFolderCell!.folderView.isHidden = true)
containerView.addSubview(toViewC.view)
containerView.addSubview(cellSnapshot)
UIView.animate(withDuration: duration, animations: {
toViewC.view.alpha = 1.0
let finalFrame = self.presenting ? projectViewC.containerView.frame : self.originFrame
cellSnapshot.frame = finalFrame
}) { (_) in
self.presenting ? (projectViewC.containerView.isHidden = false) : (self.selectedFolderCell?.isHidden = false)
cellSnapshot.removeFromSuperview()
transitionContext.completeTransition(true)
}
}
}
And the code of the first view controller that call the animation:
func animationController(forPresentedController presented: UIViewController, presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
presentAnimator.presenting = true
presentAnimator.originFrame = openingFrame!
presentAnimator.selectedFolderCell = selectedCell!
return presentAnimator
}
func animationController(forDismissedController dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
presentAnimator.presenting = false
return presentAnimator
}
Use UIViewPropertyAnimator.runningPropertyAnimator instead UIView.animate
There was a bug in the first beta of Xcode 8. It was resolved on the second beta.
I want to use interactive transitions in my app. I have two view controllers. And when user touches a button in first view controller I am presenting second view controller modally. My custom animation is working well but interactive transition is not working. I added a gesture to left edge of screen and when I pan from left edge second view controller is presenting but not interactive it is working as same as touching to button for presenting.
My class:
class MenuTransitionManager: UIPercentDrivenInteractiveTransition, UIViewControllerAnimatedTransitioning {
private var interactive = false
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 2.5
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
let finalFrameForVC = transitionContext.finalFrameForViewController(toViewController)
let containerView = transitionContext.containerView()
let bounds = UIScreen.mainScreen().bounds
toViewController.view.frame = CGRectOffset(finalFrameForVC, 0, bounds.size.height)
containerView!.addSubview(toViewController.view)
UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: .CurveLinear, animations: {
fromViewController.view.alpha = 0.5
toViewController.view.frame = finalFrameForVC
}, completion: {
finished in
transitionContext.completeTransition(true)
fromViewController.view.alpha = 1.0
})
}
func interactionControllerForPresentation(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
// if our interactive flag is true, return the transition manager object
// otherwise return nil
return self.interactive ? self : nil
}
func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return self.interactive ? self : nil
}
private var enterPanGesture: UIScreenEdgePanGestureRecognizer!
var sourceViewController: UIViewController! {
didSet {
self.enterPanGesture = UIScreenEdgePanGestureRecognizer()
self.enterPanGesture.addTarget(self, action:"handleOnstagePan:")
self.enterPanGesture.edges = UIRectEdge.Left
self.sourceViewController.view.addGestureRecognizer(self.enterPanGesture)
}
}
func handleOnstagePan(pan: UIPanGestureRecognizer){
// how much distance have we panned in reference to the parent view?
let translation = pan.translationInView(pan.view!)
// do some math to translate this to a percentage based value
let d = translation.x / CGRectGetWidth(pan.view!.bounds) * 0.5
// now lets deal with different states that the gesture recognizer sends
switch (pan.state) {
case UIGestureRecognizerState.Began:
// set our interactive flag to true
self.interactive = true
// trigger the start of the transition
self.sourceViewController.performSegueWithIdentifier("showAction", sender: self)
break
case UIGestureRecognizerState.Changed:
// update progress of the transition
self.updateInteractiveTransition(d)
break
default: // .Ended, .Cancelled, .Failed ...
// return flag to false and finish the transition
self.interactive = false
self.finishInteractiveTransition()
}
}
}
My first view controller:
override func viewDidLoad() {
super.viewDidLoad()
self.transitionManager.sourceViewController = self
}
var transitionManager = MenuTransitionManager()
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return transitionManager
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showAction" {
let toViewController = segue.destinationViewController as UIViewController
toViewController.transitioningDelegate = self
toViewController.modalPresentationStyle = .Custom
}
}
How can I fix it?
You also need to implement interactionControllerForPresentation(_:) on the view controller and vend an instance of UIViewControllerInteractiveTransitioning (which is a sub-protocol of UIPercentDrivenInteractiveTransition that your MenuTransitionManager class already implements).
The documentation around this subject is actually pretty good.
I have came across really cool transitions between viewControllers since UIViewControllerAnimatedTransitioning protocol was made available in IOS 7. Recently I noticed a particularly interesting one in Intacart's IOS app.
Here is the animation I am talking about in slow motion:
https://www.dropbox.com/s/p2hxj45ycq18i3l/Video%20Oct%2015%2C%207%2023%2059%20PM.mov?dl=0
First I thought it was similar to what the author walks through in this tutorial, with some extra fade-in and fade-out animations: http://www.raywenderlich.com/113845/ios-animation-tutorial-custom-view-controller-presentation-transitions
But then if you look at it closely, it seems like the product image transitions between the two viewControllers as the first viewController fades out. The reason why I think there are two viewControllers is because when you swipe the new view down, you can still see the original view behind it with no layout changes.
Maybe two viewControllers actually have the product image (not faded out) and are somehow animating at the same time with perfect precision and one of them fades in as the other fades out.
What do you think is actually going on there?
How is it possible to program such a transition animation that it looks like an image is shared between two viewControllers?
Here is what we did in order to achieve floating screenshot of the view during animated transition (Swift 4):
Idea behind:
Source and destination view controllers conforms to InterViewAnimatable protocol. We are using this protocol to find source and destination views.
Then we creating snapshots of those views and hiding them. Instead, at the same position snapshots are shown.
Then we animating snapshots.
At the end of transition we unhiding destination view.
As result it looks like source view is flying to destination.
File: InterViewAnimation.swift
// TODO: In case of multiple views, add another property which will return some unique string (identifier).
protocol InterViewAnimatable {
var targetView: UIView { get }
}
class InterViewAnimation: NSObject {
var transitionDuration: TimeInterval = 0.25
var isPresenting: Bool = false
}
extension InterViewAnimation: UIViewControllerAnimatedTransitioning {
func transitionDuration(using: UIViewControllerContextTransitioning?) -> TimeInterval {
return transitionDuration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
guard
let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to) else {
transitionContext.completeTransition(false)
return
}
guard let fromTargetView = targetView(in: fromVC), let toTargetView = targetView(in: toVC) else {
transitionContext.completeTransition(false)
return
}
guard let fromImage = fromTargetView.caSnapshot(), let toImage = toTargetView.caSnapshot() else {
transitionContext.completeTransition(false)
return
}
let fromImageView = ImageView(image: fromImage)
fromImageView.clipsToBounds = true
let toImageView = ImageView(image: toImage)
toImageView.clipsToBounds = true
let startFrame = fromTargetView.frameIn(containerView)
let endFrame = toTargetView.frameIn(containerView)
fromImageView.frame = startFrame
toImageView.frame = startFrame
let cleanupClosure: () -> Void = {
fromTargetView.isHidden = false
toTargetView.isHidden = false
fromImageView.removeFromSuperview()
toImageView.removeFromSuperview()
}
let updateFrameClosure: () -> Void = {
// https://stackoverflow.com/a/27997678/1418981
// In order to have proper layout. Seems mostly needed when presenting.
// For instance during presentation, destination view does'n account navigation bar height.
toVC.view.setNeedsLayout()
toVC.view.layoutIfNeeded()
// Workaround wrong origin due ongoing layout process.
let updatedEndFrame = toTargetView.frameIn(containerView)
let correctedEndFrame = CGRect(origin: updatedEndFrame.origin, size: endFrame.size)
fromImageView.frame = correctedEndFrame
toImageView.frame = correctedEndFrame
}
let alimationBlock: (() -> Void)
let completionBlock: ((Bool) -> Void)
fromTargetView.isHidden = true
toTargetView.isHidden = true
if isPresenting {
guard let toView = transitionContext.view(forKey: .to) else {
transitionContext.completeTransition(false)
assertionFailure()
return
}
containerView.addSubviews(toView, toImageView, fromImageView)
toView.frame = CGRect(origin: .zero, size: containerView.bounds.size)
toView.alpha = 0
alimationBlock = {
toView.alpha = 1
fromImageView.alpha = 0
updateFrameClosure()
}
completionBlock = { _ in
let success = !transitionContext.transitionWasCancelled
if !success {
toView.removeFromSuperview()
}
cleanupClosure()
transitionContext.completeTransition(success)
}
} else {
guard let fromView = transitionContext.view(forKey: .from) else {
transitionContext.completeTransition(false)
assertionFailure()
return
}
containerView.addSubviews(toImageView, fromImageView)
alimationBlock = {
fromView.alpha = 0
fromImageView.alpha = 0
updateFrameClosure()
}
completionBlock = { _ in
let success = !transitionContext.transitionWasCancelled
if success {
fromView.removeFromSuperview()
}
cleanupClosure()
transitionContext.completeTransition(success)
}
}
// TODO: Add more precise animation (i.e. Keyframe)
if isPresenting {
UIView.animate(withDuration: transitionDuration, delay: 0, options: .curveEaseIn,
animations: alimationBlock, completion: completionBlock)
} else {
UIView.animate(withDuration: transitionDuration, delay: 0, options: .curveEaseOut,
animations: alimationBlock, completion: completionBlock)
}
}
}
extension InterViewAnimation {
private func targetView(in viewController: UIViewController) -> UIView? {
if let view = (viewController as? InterViewAnimatable)?.targetView {
return view
}
if let nc = viewController as? UINavigationController, let vc = nc.topViewController,
let view = (vc as? InterViewAnimatable)?.targetView {
return view
}
return nil
}
}
Usage:
let sampleImage = UIImage(data: try! Data(contentsOf: URL(string: "https://placekitten.com/320/240")!))
class InterViewAnimationMasterViewController: StackViewController {
private lazy var topView = View().autolayoutView()
private lazy var middleView = View().autolayoutView()
private lazy var bottomView = View().autolayoutView()
private lazy var imageView = ImageView(image: sampleImage).autolayoutView()
private lazy var actionButton = Button().autolayoutView()
override func setupHandlers() {
actionButton.setTouchUpInsideHandler { [weak self] in
let vc = InterViewAnimationDetailsViewController()
let nc = UINavigationController(rootViewController: vc)
nc.modalPresentationStyle = .custom
nc.transitioningDelegate = self
vc.handleClose = { [weak self] in
self?.dismissAnimated()
}
// Workaround for not up to date laout during animated transition.
nc.view.setNeedsLayout()
nc.view.layoutIfNeeded()
vc.view.setNeedsLayout()
vc.view.layoutIfNeeded()
self?.presentAnimated(nc)
}
}
override func setupUI() {
stackView.addArrangedSubviews(topView, middleView, bottomView)
stackView.distribution = .fillEqually
bottomView.addSubviews(imageView, actionButton)
topView.backgroundColor = UIColor.red.withAlphaComponent(0.5)
middleView.backgroundColor = UIColor.green.withAlphaComponent(0.5)
bottomView.layoutMargins = UIEdgeInsets(horizontal: 15, vertical: 15)
bottomView.backgroundColor = UIColor.yellow.withAlphaComponent(0.5)
actionButton.title = "Tap to perform Transition!"
actionButton.contentEdgeInsets = UIEdgeInsets(dimension: 8)
actionButton.backgroundColor = .magenta
imageView.layer.borderWidth = 2
imageView.layer.borderColor = UIColor.magenta.withAlphaComponent(0.5).cgColor
}
override func setupLayout() {
var constraints = LayoutConstraint.PinInSuperView.atCenter(imageView)
constraints += [
LayoutConstraint.centerX(viewA: bottomView, viewB: actionButton),
LayoutConstraint.pinBottoms(viewA: bottomView, viewB: actionButton)
]
let size = sampleImage?.size.scale(by: 0.5) ?? CGSize()
constraints += LayoutConstraint.constrainSize(view: imageView, size: size)
NSLayoutConstraint.activate(constraints)
}
}
extension InterViewAnimationMasterViewController: InterViewAnimatable {
var targetView: UIView {
return imageView
}
}
extension InterViewAnimationMasterViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animator = InterViewAnimation()
animator.isPresenting = true
return animator
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animator = InterViewAnimation()
animator.isPresenting = false
return animator
}
}
class InterViewAnimationDetailsViewController: StackViewController {
var handleClose: (() -> Void)?
private lazy var imageView = ImageView(image: sampleImage).autolayoutView()
private lazy var bottomView = View().autolayoutView()
override func setupUI() {
stackView.addArrangedSubviews(imageView, bottomView)
stackView.distribution = .fillEqually
imageView.contentMode = .scaleAspectFit
imageView.layer.borderWidth = 2
imageView.layer.borderColor = UIColor.purple.withAlphaComponent(0.5).cgColor
bottomView.backgroundColor = UIColor.blue.withAlphaComponent(0.5)
navigationItem.leftBarButtonItem = BarButtonItem(barButtonSystemItem: .done) { [weak self] in
self?.handleClose?()
}
}
}
extension InterViewAnimationDetailsViewController: InterViewAnimatable {
var targetView: UIView {
return imageView
}
}
Reusable extensions:
extension UIView {
// https://medium.com/#joesusnick/a-uiview-extension-that-will-teach-you-an-important-lesson-about-frames-cefe1e4beb0b
public func frameIn(_ view: UIView?) -> CGRect {
if let superview = superview {
return superview.convert(frame, to: view)
}
return frame
}
}
extension UIView {
/// The method drawViewHierarchyInRect:afterScreenUpdates: performs its operations on the GPU as much as possible
/// In comparison, the method renderInContext: performs its operations inside of your app’s address space and does
/// not use the GPU based process for performing the work.
/// https://stackoverflow.com/a/25704861/1418981
public func caSnapshot(scale: CGFloat = 0, isOpaque: Bool = false) -> UIImage? {
var isSuccess = false
UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, scale)
if let context = UIGraphicsGetCurrentContext() {
layer.render(in: context)
isSuccess = true
}
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return isSuccess ? image : nil
}
}
Result (gif animation):
It's probably two different views and an animated snapshot view. In fact, this is exactly why snapshot views were invented.
That's how I do it in my app. Watch the movement of the red rectangle as the presented view slides up and down:
It looks like the red view is leaving the first view controller and entering the second view controller, but it's just an illusion. If you have a custom transition animation, you can add extra views during the transition. So I create a snapshot view that looks just like the first view, hide the real first view, and move the snapshot view — and then remove the snapshot view and show the real second view.
Same thing here (such a good trick that I use it in a lot of apps): it looks like the title has come loose from the first view controller table view cell and slid up to into the second view controller, but it's just a snapshot view: