Apple discusses how to have a container view controller transition between two child view controllers in this document. I would like to animate a simple push vertical slide up identical to UIModalTransitionStyleCoverVertical in UIModalTransitionStyle. However, transitionFromViewController only allows use of UIViewAnimationOptions, not transition styles. So how would one animate sliding a view up?
It's odd that to transition between child view controllers you can't call a simple push method similar in UINavigationController to animate the transition.
Load child view, set frame with origin.y under bottom screen. After change it to 0 in animation block. Example:
enum Animation {
case LeftToRight
case RightToLeft
}
func animationForLoad(fromvc: UIViewController, tovc: UIViewController, with animation: Animation) {
self.addChildViewController(tovc)
self.container.addSubview(tovc.view)
self.currentVC = tovc
var endOriginx: CGFloat = 0
if animation == Animation.LeftToRight {
tovc.view.frame.origin.x = -self.view.bounds.width
endOriginx += fromvc.view.frame.width
} else {
tovc.view.frame.origin.x = self.view.bounds.width
endOriginx -= fromvc.view.frame.width
}
self.transition(from: fromvc, to: tovc, duration: 0.35, options: UIViewAnimationOptions.beginFromCurrentState, animations: {
tovc.view.frame = fromvc.view.frame
fromvc.view.frame.origin.x = endOriginx
}, completion: { (finish) in
tovc.didMove(toParentViewController: self)
fromvc.view.removeFromSuperview()
fromvc.removeFromParentViewController()
})
}
Above code is transition between 2 child view with push and pop horizontal animation.
Related
I have one main ViewController, which will render different views from other view controllers (mostly table views), by using addChild:Vc i can present and remove the child view, but the problem is it's a view hierarchy, so view layers will come over each other and every child view has a button which will dismiss itself and re-presents the previous view in view hierarchy. exactly like Navigation Bar back button.
So far what i have done is an UIViewController Extension which is:
func addChildVC(_ child: UIViewController,
centerWith center: CGPoint? = CGPoint(x: 0.0, y: 0.0),
insertInView insertIn: UIView? = nil,
transition: UIView.AnimationOptions? = [],
completion: ((Bool) -> Void)? = nil)
{
self.addChild(child)
if let center = center
{
child.view.center = center
}
if let insertIn = insertIn
{
insertIn.insertSubview(child.view, aboveSubview: insertIn.self)
} else {
self.view.addSubview(child.view)
}
child.didMove(toParent: self)
}
func removeChildVC()
{
willMove(toParent: nil)
view.removeFromSuperview()
removeFromParent()
}
You need a navigation for the contained vc
Step 1
Step 2 select the child vc and
Step 3
Now you can push and pop inside that child vc
Using Xcode 10+, Swift 4, iOS 11.4+
First let me say that I'm not using a Navigation Controller -
I'm adding a ViewController to another as a child using this basic code:
topController.addChildViewController(childVC)
topController.view.addSubview(childVC.view)
childVC.didMove(toParentViewController: topController)
The child is smaller than the parent and has a few buttons, one of which will animate it out of view.
I'm not using present/dismiss as it always covers the entire screen.
I'd like it to be modal - once it's animated into place, nothing else on screen (behind it) should be usable until it is animated out of view.
How can I make the childVC be modal?
You could try adding the controller to a UIWindow which has windowLevel = UIWindowLevelAlert + 1 instead. Then after the dismiss animation finishes you could remove the window. Here is a sample code snippet that seems to work:
func presentChildVC() {
modalWindow = UIWindow(frame: UIScreen.main.bounds)
let rootController = UIViewController()
rootController.view.backgroundColor = .clear
rootController.addChild(childController)
rootController.view.addSubview(childController.view)
childController.didMove(toParent: rootController)
modalWindow?.rootViewController = rootController
modalWindow?.windowLevel = .alert + 1
modalWindow?.makeKeyAndVisible()
modalWindow?.backgroundColor = .clear
UIView.animate(withDuration: 2, animations: {
self.childController.view.alpha = 1
})
}
func dismissChildVC() {
UIView.animate(withDuration: 2, animations: {
self.childController.view.alpha = 0
}, completion: { _ in
self.modalWindow?.isHidden = true
self.modalWindow = nil
})
}
1) The child is smaller than the parent:-
You just need to update your child view frame same like parent view.
topController.addChildViewController(childVC)
topController.view.addSubview(childVC.view)
**childVC.view.frame.size.height = self.view.frame.size.height**
childVC.didMove(toParentViewController: topController)
2) has a few buttons, one of which will animate it out of view :-
Set Click Event on buttons like this to remove child view from parent
self.willMove(toParentViewController: nil)
self.view.removeFromSuperview()
self.removeFromParentViewController()
Ok, having some Tab bar controller animation problems trying to mimic the animation transition in Apple's iTunes when they leave the music player VC. Right now I have a media bar which is just a UIView that sits on the bottom of the screen and animates up whenever I select a tab that isn't the music player view (so selectedIndex != (viewControllers?.count)!-1)
This is fine, but even looking at other tab bar animations I cant make something like the vertical/squish animation transition from iTunes. This answer gave me how to do it horizontally like a page - iPhone: How to switch tabs with an animation?
However it doesn't work when you make it vertical. I basically want the dismiss transition as if the tab were a modal view controller. What I have:
func animateToTab(toIndex: Int) {
let tabViewControllers = viewControllers!
let fromView = selectedViewController!.view
let toView = tabViewControllers[toIndex].view
let fromIndex = tabViewControllers.index(of: selectedViewController!)
guard fromIndex != toIndex else {return}
// Add the toView to the tab bar view
fromView?.superview!.addSubview(toView!)
fromView?.superview!.sendSubview(toBack: toView!)
// Disable interaction during animation
view.isUserInteractionEnabled = false
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 1, initialSpringVelocity: 0.3, options: UIViewAnimationOptions.curveEaseOut, animations: {
// Slide the views by -offset
fromView?.superview!.center.y += screenSize.height
//toView?.center = CGPoint(x: (toView?.center.x)! - offset, y: (toView?.center.y)!);
}, completion: { finished in
// Remove the old view from the tabbar view.
fromView?.removeFromSuperview()
self.view.isUserInteractionEnabled = true
})
self.selectedIndex = toIndex
}
But the current VC, the music player, snaps back to its original position after it moves and switches the selectedIndex. Its like the tab bar controller makes all its viewcontrollers be in the right position before switching.
How can I mimic a modal dismiss animation with a tab bar VC?
I have several swipe and pan gestures to navigate through some views. When I pan the view, a new ViewController will be instantiated and placed at the edge of the window. Along my pan, the view will come into view on top of the current view. If the pan has passed halfway, the new view will automatically finish and replaces the current view and on completion removing the old view.
var newViewController: UIViewController! {
didSet {
if let newView = NewViewController {
addChildViewController(newView)
newView.view.frame.origin.x = view.bounds.width }
view.addSubview(newView.view)
newView.didMoveToParentViewController(self)
}
}
}
var currentViewController: UIViewController! {
didSet(oldView) {
oldViewController = oldView
newViewController = nil
}
}
var oldViewController: UIViewController!
func removeViewController() {
if let oldView = oldViewController {
oldView.willMoveToParentViewController(nil)
oldView.view.removeFromSuperview()
oldView.removeFromParentViewController()
oldViewController = nil
}
}
newViewController = NewVIewController()
currentViewController = newViewController
UIView.animateWithDuration(2.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .CurveEaseInOut, animations: {
self.currentViewController.view.frame.origin.x = 0 }) { _ in
self.removeViewController()
}
In the completion of the animation, the old view will be taken care of. But to make my app to be more responsive, I'd like to call removeViewController right away and still keeping a "snapshot" of the oldView so the transition will still look the same.
Should I use some other method for transitioning the views?
It's too late to follow the pan gesture once the swipe event has been fired. Instead consider using uiscreenedgegesturerecognizer
The problem is that this is not how you do an interactive view controller transition. You need to trigger an actual view controller transition (e.g. call presentViewController) and supply a UIViewControllerInteractiveTransitioning object.
I keep getting the error:
*** Terminating app due to uncaught exception 'NSGenericException', reason: 'Push segues can only be used when the source controller is managed by an instance of UINavigationController.'
When trying to switch to a new view controller. Below is the segue switching view controllers:
Even when I try put the name of my transition class in the Segue class, it still gives the error on my device, but works perfectly fine in the simulator.
The code for the transition class:
class TransitionManager: UIStoryboardSegue, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
private var presenting = true
// MARK: UIViewControllerAnimatedTransitioning protocol methods
// animate a change from one viewcontroller to another
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
// get reference to our fromView, toView and the container view that we should perform the transition in
let container = transitionContext.containerView()
let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!
// set up from 2D transforms that we'll use in the animation
let π : CGFloat = 3.14159265359
let offScreenRight = CGAffineTransformMakeRotation(-π/2)
let offScreenLeft = CGAffineTransformMakeRotation(π/2)
// prepare the toView for the animation
toView.transform = self.presenting ? offScreenRight : offScreenLeft
// set the anchor point so that rotations happen from the top-left corner
toView.layer.anchorPoint = CGPoint(x:0, y:0)
fromView.layer.anchorPoint = CGPoint(x:0, y:0)
// updating the anchor point also moves the position to we have to move the center position to the top-left to compensate
toView.layer.position = CGPoint(x:0, y:0)
fromView.layer.position = CGPoint(x:0, y:0)
// add the both views to our view controller
container.addSubview(toView)
container.addSubview(fromView)
// get the duration of the animation
// DON'T just type '0.5s' -- the reason why won't make sense until the next post
// but for now it's important to just follow this approach
let duration = self.transitionDuration(transitionContext)
// perform the animation!
// for this example, just slid both fromView and toView to the left at the same time
// meaning fromView is pushed off the screen and toView slides into view
// we also use the block animation usingSpringWithDamping for a little bounce
UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.8, options: nil, animations: {
// slide fromView off either the left or right edge of the screen
// depending if we're presenting or dismissing this view
fromView.transform = self.presenting ? offScreenLeft : offScreenRight
toView.transform = CGAffineTransformIdentity
}, completion: { finished in
// tell our transitionContext object that we've finished animating
transitionContext.completeTransition(true)
})
}
// return how many seconds the transiton animation will take
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
return 0.75
}
// MARK: UIViewControllerTransitioningDelegate protocol methods
// return the animataor when presenting a viewcontroller
// remmeber that an animator (or animation controller) is any object that aheres to the UIViewControllerAnimatedTransitioning protocol
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
// these methods are the perfect place to set our `presenting` flag to either true or false - voila!
self.presenting = true
return self
}
// return the animator used when dismissing from a viewcontroller
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.presenting = false
return self
}
override func perform() {
//
}
}
Is there anything I am missing? Or how can I get this to work?
Usually that error indicates that you're trying to perform a push segue, and the presenting view controller is not managed by a UINavigationController. The simple solution is usually to embed the presenting (i.e. from) view controller in a navigation controller as outlined in NSGenericException', reason: 'Push segues can only be used when the source controller is managed by an instance of UINavigationController.
But since you've rolled your own UIStoryboardSegue class, I suspect you aren't trying to simply use the default push segue animation. Without knowing how you are managing your transitioningDelegate, my guess is you need to actually override perform() to present your view controller instead of pushing it:
override func perform() {
let fromVC = sourceViewController as UIViewController
let toVC = destinationViewController as UIViewController
fromVC.presentViewController(toVC as UIViewController, animated: true, completion: nil)
}