I am using a UISplitViewController, with a MasterViewController and DetailViewController, without UINavigationControllers.
Currenly the segue animation for master->detail, triggered by
performSegueWithIdentifier("showDetail", sender: self)
consists in the DetailViewController showing up from the bottom upwards.
How can I make that animation showing left-wards?
I've recently needed to have more control of how the segues are being performed, so I made my custom segue classes which all perform the transition in different directions. Here's one of the implementations:
Swift 2.x
override func perform() {
//credits to http://www.appcoda.com/custom-segue-animations/
let firstClassView = self.sourceViewController.view
let secondClassView = self.destinationViewController.view
let screenWidth = UIScreen.mainScreen().bounds.size.width
let screenHeight = UIScreen.mainScreen().bounds.size.height
secondClassView.frame = CGRectMake(screenWidth, 0, screenWidth, screenHeight)
if let window = UIApplication.sharedApplication().keyWindow {
window.insertSubview(secondClassView, aboveSubview: firstClassView)
UIView.animateWithDuration(0.4, animations: { () -> Void in
firstClassView.frame = CGRectOffset(firstClassView.frame, -screenWidth, 0)
secondClassView.frame = CGRectOffset(secondClassView.frame, -screenWidth, 0)
}) {(Finished) -> Void in
self.sourceViewController.navigationController?.pushViewController(self.destinationViewController, animated: false)
}
}
}
This one will have a "right to left" transition. You can modify this function for your needs by simply changing the initial and ending positions of the source and destination view controller.
Also don't forget that you need to mark your segue as "custom segue", and to assign the new class to it.
UPDATE: Added Swift 3 version
Swift 3
override func perform() {
//credits to http://www.appcoda.com/custom-segue-animations/
let firstClassView = self.source.view
let secondClassView = self.destination.view
let screenWidth = UIScreen.main.bounds.size.width
let screenHeight = UIScreen.main.bounds.size.height
secondClassView?.frame = CGRect(x: screenWidth, y: 0, width: screenWidth, height: screenHeight)
if let window = UIApplication.shared.keyWindow {
window.insertSubview(secondClassView!, aboveSubview: firstClassView!)
UIView.animate(withDuration: 0.4, animations: { () -> Void in
firstClassView?.frame = (firstClassView?.frame.offsetBy(dx: -screenWidth, dy: 0))!
secondClassView?.frame = (secondClassView?.frame.offsetBy(dx: -screenWidth, dy: 0))!
}, completion: {(Finished) -> Void in
self.source.navigationController?.pushViewController(self.destination, animated: false)
})
}
}
Embed your view controllers in UINavigationControllers.
Per the SplitViewController template:
On smaller devices it's going to have to use the Master's navigationController.
Furthermore this question has been answered here and here and here
More from the View Controller Programming Guide:
There are two ways to display a view controller onscreen: embed it in
a container view controller or present it. Container view controllers
provide an app’s primary navigation….
In storyboards it's not that difficult to embed something in a navigation controller. Click on the view controller you want to embed, then Editor->embed in->navigation controller.
--Swift 3.0--
Armin's solution adapted for swift 3.
New -> File -> Cocoa Touch Class -> Class: ... (Subclass: UIStoryboardSegue).
import UIKit
class SlideHorSegue: UIStoryboardSegue {
override func perform() {
//credits to http://www.appcoda.com/custom-segue-animations/
let firstClassView = self.source.view
let secondClassView = self.destination.view
let screenWidth = UIScreen.main.bounds.size.width
let screenHeight = UIScreen.main.bounds.size.height
secondClassView?.frame = CGRect(x: screenWidth, y: 0, width: screenWidth, height: screenHeight)
if let window = UIApplication.shared.keyWindow {
window.insertSubview(secondClassView!, aboveSubview: firstClassView!)
UIView.animate(withDuration: 0.4, animations: { () -> Void in
firstClassView?.frame = (firstClassView?.frame)!.offsetBy(dx: -screenWidth, dy: 0)
secondClassView?.frame = (secondClassView?.frame)!.offsetBy(dx: -screenWidth, dy: 0)
}) {(Finished) -> Void in
self.source.navigationController?.pushViewController(self.destination, animated: false)
}
}
}
}
In storyboard: mark your segue as "custom segue", and to assign the new class to it.
Note: If you have a UIScrollView in your detailesVC, this won't work.
It sounds as though the new view controller is presenting modally. If you embed the detailViewController into a UINavigationController and push the new controller it will animate from right to left and should show a back button too by default.
When you have a compact-width screen,"Show Detail" segue fall back to modal segue automatically.So your DetailViewController will appear vertically as the default modal segue.
You can use a UIViewControllerTransitioningDelegate to custom the animation of a modal segue.
Here is an example to achieve the horizontal animation:
1. set the transitioningDelegate and it's delegate method
class MasterViewController: UITableViewController,UIViewControllerTransitioningDelegate {
//prepare for segue
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showDetail" {
let detailVC = segue.destinationViewController as! DetailViewController
detailVC.transitioningDelegate = self
// detailVC.detailItem = object//Configure detailVC
}
}
//UIViewControllerTransitioningDelegate
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return LeftTransition()
}
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let leftTransiton = LeftTransition()
leftTransiton.dismiss = true
return leftTransiton
}
}
2: a custom UIViewControllerAnimatedTransitioning : LeftTransition
import UIKit
class LeftTransition: NSObject ,UIViewControllerAnimatedTransitioning {
var dismiss = false
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 2.0
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning){
// Get the two view controllers
let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
let containerView = transitionContext.containerView()!
var originRect = containerView.bounds
originRect.origin = CGPointMake(CGRectGetWidth(originRect), 0)
containerView.addSubview(fromVC.view)
containerView.addSubview(toVC.view)
if dismiss{
containerView.bringSubviewToFront(fromVC.view)
UIView.animateWithDuration(transitionDuration(transitionContext), animations: { () -> Void in
fromVC.view.frame = originRect
}, completion: { (_ ) -> Void in
fromVC.view.removeFromSuperview()
transitionContext.completeTransition(true )
})
}else{
toVC.view.frame = originRect
UIView.animateWithDuration(transitionDuration(transitionContext),
animations: { () -> Void in
toVC.view.center = containerView.center
}) { (_) -> Void in
fromVC.view.removeFromSuperview()
transitionContext.completeTransition(true )
}
}
}
}
Related
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. :)
I made a custom segue that moves the current view to the left and moves the next view in from the right at the same time. When I used it on the first segueway it works perfectly, but when used in the next segueway only the destination view moves. The code for the segue looks like the following:
class CustomSlideSegue: UIStoryboardSegue {
override func perform() {
let firstVCView = self.sourceViewController.view as UIView!
let secondVCView = self.destinationViewController.view as UIView!
let screenWidth = UIScreen.mainScreen().bounds.size.width
let screenHeight = UIScreen.mainScreen().bounds.size.height
secondVCView.frame = CGRectMake(screenWidth, 0, screenWidth, screenHeight)
let window = UIApplication.sharedApplication().keyWindow
window?.insertSubview(secondVCView, aboveSubview: firstVCView)
UIView.animateWithDuration(0.6, animations: { () -> Void in
firstVCView.transform = CGAffineTransformMakeTranslation(-screenWidth, 0)
secondVCView.transform = CGAffineTransformMakeTranslation(-screenWidth, 0)
}) { (Finished) -> Void in
self.sourceViewController.presentViewController(self.destinationViewController as UIViewController, animated: false, completion: nil)
}
}
}
And here is the code from the view controller button action method where I start the segue:
performSegueWithIdentifier("customSlideSegue", sender: self)
Here is a video showing the segues:
Can anyone see the problem? Or how should I go about debugging this? Thankful for replies!
the problem was that the second time you call the segue the sourceviewcontroller's view's transform already has a value of -screenWidth and thus it doesn't get pushed out of the screen. change your code to the following:
override func perform() {
let firstVCView = self.sourceViewController.view as UIView!
let secondVCView = self.destinationViewController.view as UIView!
let screenWidth = UIScreen.mainScreen().bounds.size.width
secondVCView.transform = CGAffineTransformMakeTranslation(screenWidth, 0)
let window = UIApplication.sharedApplication().keyWindow
window?.insertSubview(secondVCView, aboveSubview: firstVCView)
UIView.animateWithDuration(0.6, animations: { () -> Void in
firstVCView.transform = CGAffineTransformMakeTranslation(-screenWidth, 0)
secondVCView.transform = CGAffineTransformIdentity
}) { (Finished) -> Void in
self.sourceViewController.presentViewController(self.destinationViewController, animated: false, completion: nil)
}
}
I want to navigate tru ViewControllers with animations like in PageViewController if you choose Scroll style. So I want to navigate tru them using left/right swipes.
How to do it without UIPageViewController.
Is there a way to develop reusable class which could be used as main transition style in my app ?
I appreciate any help
If you want the same effect as UIPageViewController you should just use it. That's what it's there for. If you have a real reason for not using it then you can google any of the many tutorials for implementing a container view controller.
Well, I managed to do it using Custom Storyboard Segues
Here is the code :
Custom segue :
import UIKit
class CustomScrollPagingSegue: UIStoryboardSegue {
override func perform() {
let firstVCView = self.sourceViewController.view as UIView!
let secondVCView = self.destinationViewController.view as UIView!
let screenWidth = UIScreen.mainScreen().bounds.size.width
let screenHeight = UIScreen.mainScreen().bounds.size.height
secondVCView.frame = CGRectMake(screenWidth, 0.0, screenWidth, screenHeight)
let window = UIApplication.sharedApplication().keyWindow
window?.insertSubview(secondVCView, aboveSubview: firstVCView)
UIView.animateWithDuration(0.5, animations: { () -> Void in
firstVCView.frame = CGRectOffset(firstVCView.frame, -screenWidth, 0.0)
secondVCView.frame = CGRectOffset(secondVCView.frame, -screenWidth, 0.0)
}) { (Finished) -> Void in
self.sourceViewController.presentViewController(self.destinationViewController as UIViewController, animated: false, completion: nil)
}
}
}
and Custom Unwind Segue
import UIKit
class CustomScrollPagingSegueUnwind: UIStoryboardSegue {
override func perform() {
// Assign the source and destination views to local variables.
let secondVCView = self.sourceViewController.view as UIView!
let firstVCView = self.destinationViewController.view as UIView!
let screenWidth = UIScreen.mainScreen().bounds.size.width
let window = UIApplication.sharedApplication().keyWindow
window?.insertSubview(firstVCView, aboveSubview: secondVCView)
// Animate the transition.
UIView.animateWithDuration(0.5, animations: { () -> Void in
firstVCView.frame = CGRectOffset(firstVCView.frame, screenWidth, 0.0)
secondVCView.frame = CGRectOffset(secondVCView.frame, screenWidth, 0.0)
}) { (Finished) -> Void in
self.sourceViewController.dismissViewControllerAnimated(false, completion: nil)
}
}
}
I think having 100% same result is possible with interactive transitions, but I don't know how to implement them.
When swipe , i want navigate between pages with smoothly ( change according to finger moves ) not to navigate with a given time
class FirstCustomSegue: UIStoryboardSegue {
override func perform() {
// Assign the source and destination views to local variables.
var firstVCView = self.sourceViewController.view as UIView!
var secondVCView = self.destinationViewController.view as UIView!
// Get the screen width and height.
let screenWidth = UIScreen.mainScreen().bounds.size.width
let screenHeight = UIScreen.mainScreen().bounds.size.height
// Specify the initial position of the destination view.
secondVCView.frame = CGRectMake(screenWidth, 0.0, screenWidth, screenHeight)
// Access the app's key window and insert the destination view above the current (source) one.
let window = UIApplication.sharedApplication().keyWindow
window?.insertSubview(secondVCView, aboveSubview: firstVCView)
// Animate the transition.
UIView.animateWithDuration(0.4, animations: { () -> Void in
firstVCView.frame = CGRectOffset(firstVCView.frame, -screenWidth, 0.0)
secondVCView.frame = CGRectOffset(secondVCView.frame, -screenWidth, 0.0)
}) { (Finished) -> Void in
self.sourceViewController.presentViewController(self.destinationViewController as! UIViewController,
animated: false,
completion: nil)
}
} }
I had to deal with it recently. look at my github project, maybe it will hepl you.
If a nutshell. You should create class adopts
UIViewControllerAnimatedTransitioning
and implement 2 methods. One for animation's duration, another for your custom animation (moving, fade, and so on).
For example:
class ModalPresentAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.5
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
// ...
let duration = transitionDuration(transitionContext)
UIView.animateWithDuration(duration, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.2, options: UIViewAnimationOptions.CurveLinear, animations: {
toVC.view.frame = // new postion
}) { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
}
Then in your ViewController to specify a new ViewControllerTransitioning
extension ViewController: UIViewControllerTransitioningDelegate {
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
swipeInteractionController.wireToViewController(presented)
return modalPresentAnimationController
}
But, if you want to add "change according to finger moves" you should create class adopts
UIPercentDrivenInteractiveTransition
And in it to use gestureRecognizer.
I have two views I would like to make a swipe style transition accross and I have done that when there is a segue to act on but the I don't have one here so am not sure how to apply my animation class. All I have in my class is:
let stb = UIStoryboard(name: "Walkthrough", bundle: nil)
let walkthrough = stb.instantiateViewControllerWithIdentifier("walk") as! BWWalkthroughViewController
self.presentViewController(walkthrough, animated: true, completion: nil)
I want to apply apply the following custom segue:
class CustomSegue: UIStoryboardSegue {
override func perform() {
// Assign the source and destination views to local variables.
var firstVCView = self.sourceViewController.view as UIView!
var secondVCView = self.destinationViewController.view as UIView!
// Get the screen width and height.
let screenWidth = UIScreen.mainScreen().bounds.size.width
let screenHeight = UIScreen.mainScreen().bounds.size.height
// Specify the initial position of the destination view.
secondVCView.frame = CGRectMake(0.0, screenHeight, screenWidth, screenHeight)
// Access the app's key window and insert the destination view above the current (source) one.
let window = UIApplication.sharedApplication().keyWindow
window?.insertSubview(secondVCView, aboveSubview: firstVCView)
// Animate the transition.
UIView.animateWithDuration(0.2, animations: { () -> Void in
firstVCView.frame = CGRectOffset(firstVCView.frame, -screenWidth, 0.0)
secondVCView.frame = CGRectOffset(secondVCView.frame, -screenWidth, 0.0)
}) { (Finished) -> Void in
self.sourceViewController.presentViewController(self.destinationViewController as! UIViewController,
animated: false,
completion:nil)
}
}
}
I cannot get it to work any pointers please?
While the first answer should be "use Storyboard Segues", you can solve custom transitions this way:
Generic Approach
Modify your CustomSegue to adopt both UIStoryboardSegue and UIViewControllerAnimatedTransitioning protocols.
Refactor CustomSegue so that the animation can be used by both protocols.
Setup a delegate to the navigation controller, which can be itself, to supply the custom transitions to push & pop
let animationControllerForOperation create and return an instance of CustomSegue, with an identifier of your choice.
Overview
// Adopt both protocols
class CustomSegue: UIStoryboardSegue, UIViewControllerAnimatedTransitioning {
func animate(firstVCView:UIView,
secondVCView:UIView,
containerView:UIView,
transitionContext: UIViewControllerContextTransitioning?) {
// factored transition code goes here
}) { (Finished) -> Void in
if let context = transitionContext {
// UIViewControllerAnimatedTransitioning
} else {
// UIStoryboardSegue
}
}
}
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
// return timing
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
// Perform animate using transitionContext information
// (UIViewControllerAnimatedTransitioning)
}
override func perform() {
// Perform animate using segue (self) variables
// (UIStoryboardSegue)
}
}
Below is the complete code example. It has been tested. Notice that I also fixed a few animation bugs from the original question, and added a fade-in effect to emphasize the animation.
CustomSegue Class
// Animation for both a Segue and a Transition
class CustomSegue: UIStoryboardSegue, UIViewControllerAnimatedTransitioning {
func animate(firstVCView:UIView,
secondVCView:UIView,
containerView:UIView,
transitionContext: UIViewControllerContextTransitioning?) {
// Get the screen width and height.
let offset = secondVCView.bounds.width
// Specify the initial position of the destination view.
secondVCView.frame = CGRectOffset(secondVCView.frame, offset, 0.0)
firstVCView.superview!.addSubview(secondVCView)
secondVCView.alpha = 0;
// Animate the transition.
UIView.animateWithDuration(self.transitionDuration(transitionContext!),
animations: { () -> Void in
firstVCView.frame = CGRectOffset(firstVCView.frame, -offset, 0.0)
secondVCView.frame = CGRectOffset(secondVCView.frame, -offset, 0.0)
secondVCView.alpha = 1; // emphasis
}) { (Finished) -> Void in
if let context = transitionContext {
context.completeTransition(!context.transitionWasCancelled())
} else {
self.sourceViewController.presentViewController(
self.destinationViewController as! UIViewController,
animated: false,
completion:nil)
}
}
}
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
return 4 // four seconds
}
// Perform Transition (UIViewControllerAnimatedTransitioning)
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
self.animate(transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!.view,
secondVCView: transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!.view,
containerView: transitionContext.containerView(),
transitionContext: transitionContext)
}
// Perform Segue (UIStoryboardSegue)
override func perform() {
self.animate(self.sourceViewController.view!!,
secondVCView: self.destinationViewController.view!!,
containerView: self.sourceViewController.view!!.superview!,
transitionContext:nil)
}
}
Host ViewController Class
class ViewController: UIViewController, UINavigationControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.delegate = self
}
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
switch operation {
case .Push:
return CustomSegue(identifier: "Abc", source: fromVC, destination: toVC)
default:
return nil
}
}
}