45

I've read the other posts on segues but none solve my question.

Simply put, my ViewControllers are ordered, like a book. I want backward transitions (example: from page 9 to 8) to always present (slide over) from left to right. I want forward transitions (from page 9 to 10) to present from right to left.

Yes, my nav-controller back button (top left) presents like this if you are paging through page by page. However, if you jump in from an Index then the back function on the nav controller takes you back to the index.

My objective is that if a user jumps to page 9 (for example) from an index, then swipes right, it'll flick the page off to the right and show page 8. Once on page 8, if they flick left the page will get flicked off to the left and they'll be on page 9 again.

All my ViewControllers are by, default, presenting by sliding in from right to left.

Example: Think of it like a book, if I use the Index to hop to chapter 4, then swipe right and pop a view from the stack, I'll be back at the Index. But if you're on Chapter 4, page 394 and you swipe right, you don't want to go back to the index! You want to go to the last page of chapter 3, page 393! So the nav stack is no help to me.

End Example

Details: 1. I'm using the new Xcode "Show" on button-tap to switch between ViewControllers.

  1. I'm using a navigation controller, for the top-left "Back" button functionality. This uses the nav stack and works fine.

  2. However I have my own custom nav-bar at the bottom (Back Button, Home Button, Index Button, Forward Button) and gestures. These are what I want to have book-like functionality with.

  3. Coding in swift.

  4. Working with Xcode 6.3

I've read that there's animation code. I've read there's in depth programatic transitions that can be used. It just seems crazy that there's no simple way to just select the segues I want to present from the left and easily reverse the animation.

Thanks!

Attempt log:

I tried DCDC's code:
    UIView.transitionWithView(self.window!, duration: 0.5, options:.TransitionFlipFromLeft, animations: { () -> Void in
            self.window!.rootViewController = mainVC
            }, completion:nil)

This error is returned when I insert DCDC's code into an IBAction for my back-swipe

This error is returned when I insert DCDC's code into an IBAction for my back-swipe

Mykola
  • 3,343
  • 6
  • 23
  • 39
Dave G
  • 12,042
  • 7
  • 57
  • 83
  • this is a bit weird because the default behaviour of the back button in show segue is to present previous controller with left to right animation. So you click back and it animates from right to left as it was a new segue to another view controller, right? Could you provide some screenshots or code you have? – theDC Jun 10 '15 at 18:31
  • 1
    I'm sorry, I misspoke. The nav-controller back button does present left to right. But how do I make my custom nav buttons do the same? For example, how do I make my custom menu back button at the bottom present from left to right? And how do I make my gesture recognizer swipe right function present from left to right? – Dave G Jun 11 '15 at 02:06

12 Answers12

92

This is how I achieve the effect without requiring a nav-controller. Try this instead:

Swift 4:

import UIKit
class SegueFromLeft: UIStoryboardSegue {
    override func perform() {
        let src = self.source
        let dst = self.destination

        src.view.superview?.insertSubview(dst.view, aboveSubview: src.view)
        dst.view.transform = CGAffineTransform(translationX: -src.view.frame.size.width, y: 0)

        UIView.animate(withDuration: 0.25,
                              delay: 0.0,
                            options: .curveEaseInOut,
                         animations: {
                                dst.view.transform = CGAffineTransform(translationX: 0, y: 0)
                                },
                        completion: { finished in
                                src.present(dst, animated: false, completion: nil)
                                    }
                        )
    }
}

Swift 3:

import UIKit

class SegueFromLeft: UIStoryboardSegue
{
    override func perform()
    {
        let src = self.sourceViewController
        let dst = self.destinationViewController

        src.view.superview?.insertSubview(dst.view, aboveSubview: src.view)
        dst.view.transform = CGAffineTransformMakeTranslation(-src.view.frame.size.width, 0)

        UIView.animateWithDuration(0.25,
            delay: 0.0,
            options: UIViewAnimationOptions.CurveEaseInOut,
            animations: {
                dst.view.transform = CGAffineTransformMakeTranslation(0, 0)
            },
            completion: { finished in
                src.presentViewController(dst, animated: false, completion: nil)
            }
        )
    }
}

Then in the storyboard, click on the segue you'd like to change. In the attributes inspector change the type to 'Custom' and change the class to 'SegueFromLeft'

jugutier
  • 179
  • 1
  • 13
Michael Hudson
  • 1,330
  • 4
  • 21
  • 25
  • 6
    It's worth to note that only with this solution, I was able to really have a "fromLeft" transition. In the accepted answer, the new view is pushed up from the button, while the old view is pushed out to the right. In this solution, the old view sticks to its position, while the new view is pushed in from the left. – chuky Feb 26 '16 at 15:24
  • 3
    just like @chuky, this solution should be the accepted answer. I experienced similar issues as BenNov where animation would play, but controller still would come from the bottom – mike.tihonchik Apr 12 '16 at 17:24
  • 3
    To have a "fromRight" transition, just remove the minus sign so it should be `dst.view.transform = CGAffineTransformMakeTranslation(src.view.frame.size.width, 0)` – Protocole Feb 09 '17 at 08:40
  • Please explain how to use this code and where, Thank you. – Dory Daniel May 22 '17 at 09:17
  • 1
    @dorydaniel See the additional note added to the very end :) – Michael Hudson May 22 '17 at 11:22
  • 2
    @MichaelHudson, I made a new class file and that worked. However, now all the other segues are using bottom-top except the one where I set the custom segue. Before it would be Right-left, whats that about? EDIT: It seems it starts using bottom-top segue right after I click the button which uses the custom segue - then all the rest will be bottom-top except the segue with the custom one. – Anders Jun 17 '17 at 20:49
  • @Anders That certainly is very strange, it shouldn't effect any other segues other than the one set to custom. Are you running beta Xcode or iOS that could have bugs and what you're seeing is just that? One option for you would be create another 3 classes for right-left, top-down, down-up and customise all the segues to your preference . (Those classes would be very slights mods on the above that I can help with if you need). Not much else I can offer other than good luck, please let me know if you figure out the issue you're having – Michael Hudson Jun 17 '17 at 23:52
  • @MichaelHudson, I am using the above solution and it worked fine the only issue I notice is in the iPhone X when destination from left come it kind of jerk when presented – Ajay Singh Thakur Nov 06 '18 at 13:44
43

Here is a custom-segue class you can use to perform a left-to-right segue in Swift. It requires QuartzCore framework and minimal animation.

Swift 2.2

import UIKit
import QuartzCore

class SegueFromLeft: UIStoryboardSegue {

    override func perform() {
        let src: UIViewController = self.sourceViewController
        let dst: UIViewController = self.destinationViewController
        let transition: CATransition = CATransition()
        let timeFunc : CAMediaTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        transition.duration = 0.25
        transition.timingFunction = timeFunc
        transition.type = kCATransitionPush
        transition.subtype = kCATransitionFromLeft
        src.navigationController!.view.layer.addAnimation(transition, forKey: kCATransition)
        src.navigationController!.pushViewController(dst, animated: false)
    }

}

Swift 3.0

import UIKit
import QuartzCore

class SegueFromLeft: UIStoryboardSegue {

    override func perform() {
        let src: UIViewController = self.source
        let dst: UIViewController = self.destination
        let transition: CATransition = CATransition()
        let timeFunc : CAMediaTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        transition.duration = 0.25
        transition.timingFunction = timeFunc
        transition.type = kCATransitionPush
        transition.subtype = kCATransitionFromLeft
        src.navigationController!.view.layer.add(transition, forKey: kCATransition)
        src.navigationController!.pushViewController(dst, animated: false)
    }
}

The "Back" button will still appear in the navigation bar on the transitioned view controller, but you can easily disable/configure the navigation controller for that view controller.

Olivia Brown
  • 266
  • 1
  • 10
Michael Ramos
  • 5,523
  • 8
  • 36
  • 51
  • Woohoo! A month after posting, someone finally posts the code I thought must exist! Simple, easy, works perfect. I added the "SegueFromLeft" class code you gave, selected the segue I wanted to present from left, selected custom, pointed it at "SegueFromLeft", and the animation is dead on. Even better, it syncs up with the navigation controller, so the nav "Back" button still works. Michael, thank you very much. – Dave G Jul 17 '15 at 04:07
  • 4
    In case anyone reads this post and wants the same function, but your app doesn't use a navigation controller, just modify the two bottom lines: src.view.layer.addAnimation(transition, forKey: kCATransition) src.presentViewController(dst, animated: true, completion: nil) My new app doesn't have a nav controller but I still wanted to use this code to easily add the desired animation, tweaking those lines was all it took. Thanks again to @Michael for coming to the rescue after a month of complicated answers!! – Dave G Aug 31 '15 at 03:43
  • Thanks a lot for this reply, it works great and it really improves the UX on my application – user817851 Sep 20 '15 at 16:10
  • 1
    Update: for users coming here post-Swift 2, you don't need to cast `as! UIViewController` (unless you have custom view controllers) and replace the `var`s with `let`s – Arc676 Jan 17 '16 at 06:50
  • 1
    I tried this, while I can see the left to right animation, the new view still appears as push animation from bottom to top. The original view slides out but the new one comes from the bottom. Any suggestions? (I've set it to custom) – BenNov Feb 20 '16 at 10:03
  • 1
    it doesn't give the same feel as the default Right --> Left transition... some fine tuning maybe required.... – George Asda Feb 24 '16 at 17:23
  • 1
    The new view is push from right-bottom to left-top, as George mentioned, the animation requires some tunings. – Kaihua Nov 25 '16 at 19:52
  • @GeorgeAsda you're right, it doesn't. Any way to achieve same effect of right-left transition? – dispatchswift May 03 '18 at 06:25
11

Updated accepted answer in Swift 3:

import UIKit

class SegueFromLeft: UIStoryboardSegue
{
    override func perform()
    {
        let src = self.source
        let dst = self.destination

        src.view.superview?.insertSubview(dst.view, aboveSubview: src.view)
        dst.view.transform = CGAffineTransform(translationX: -src.view.frame.size.width, y: 0)

        UIView.animate(withDuration: 0.25,
            delay: 0.0,
            options: UIViewAnimationOptions.curveEaseInOut,
            animations: {
                dst.view.transform = CGAffineTransform(translationX: 0, y: 0)
            },
            completion: { finished in
                src.present(dst, animated: false, completion: nil)
            }
        )
    }
}
Mark Barrasso
  • 649
  • 9
  • 17
4

You can use of the predeclared type of transition in transitionWithView method

UIView.transitionWithView(self.window!, duration: 0.5, options:.TransitionFlipFromLeft, animations: { () -> Void in
            self.window!.rootViewController = mainVC
            }, completion:nil)

I guess .TransitionFlipFromLeft is the desired one

To complete the task, drag a new view controller to your storyboard and name it with some id. This is going to be the destination of the transition.

then instantiate this view controller from code

let storyboard = UIStoryboard(name: "MyStoryboardName", bundle: nil)
let mainVC = storyboard.instantiateViewControllerWithIdentifier("someViewController") as! UIViewController

You can do this either inside the IBAction or for example in ViewDidLoad but probably IBAction will be the better choice. Pay attention to type the correct identifiers for both storyboard and view controller. Also, you have to declare your appDelegate instance. Here is the implemented IBAction

@IBAction func push(sender: UIButton) {

    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let mainVC = storyboard.instantiateViewControllerWithIdentifier("secondVC") as! UIViewController
    let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate

    UIView.transitionWithView(appDelegate.window!, duration: 0.5, options: .TransitionFlipFromLeft , animations: { () -> Void in
        appDelegate.window!.rootViewController = mainVC
        }, completion:nil)
}

If this is not what you have expected, probably you might want to make a custom animation. This article is really helpful : http://mathewsanders.com/animated-transitions-in-swift/

theDC
  • 6,364
  • 10
  • 56
  • 98
  • Where would I put that code? I don't want the view to always load from one direction. If I arrive at the target page from the page after it, I want it to be presented from the left. If I present the target page from the page before it, I want it to present from right. I can't use a nav-stack-pop. Think of it like a book, if I use the Index to hop to chapter 4, then from that page I swipe right, I don't want to go back to the Index, I want it to bring me to the last page of chapter 3. – Dave G Jun 12 '15 at 02:01
  • Hi DCDC, I've done a lot of research and I think your answer is the closest thing to what I want. Does that goes simply go into the target/next ViewController's class? So every ViewController would have this code, specifying that if it is loaded from x VC, transition from the left? – Dave G Jun 24 '15 at 03:22
  • I'm afraid I do not understand your question. This code does a transition to the a viewController, which must be instantiated before you call this transition function. If this works for you, don't forget to accept my answer:) – theDC Jun 24 '15 at 19:37
  • I guess I'm asking how exactly to implement this. Does this code go into the view did load of the new view controller? It seems line no. So then where does it go? – Dave G Jun 25 '15 at 15:30
  • You can put this code inside the IBAction of the aforementioned button. Then it would present the desired view controller – theDC Jun 25 '15 at 15:59
  • Ahh, sorry, I know I'm a big noob. I just option drag from the button to the view. Putting the code in the IBAction didn't strike me because I've never done it that way. I'll try it and check this as the answe if it works. – Dave G Jun 26 '15 at 08:53
  • DCDC, I created an IBAction for my right/backward swipe gesture. I then pasted the code into the IBAction. Last, I changed the name of "mainVC" to the current view controller. The 'self.window' seems to be causing an error. I added a screenshot to the post. What is that code referring to? You mentioned initiating the upcoming VC before calling this function but I don't know how I'd do that besides the gesture segue doing it by default. Maybe the error has to do with my not understanding that part? – Dave G Jul 02 '15 at 03:59
  • I don't notice your code making any difference in my app's behavior. I've triple checked I did as you said. The one thing I do notice is that this piece of code "options: .TransitionFlipFromLeft" does not get color coded by X-Code, it's just black. When I delete and retype it, X-Code does not auto-finish it. Obviously "flip from left" is a key part of my goal! Any ideas as to why X-Code isn't recognizing it? – Dave G Jul 02 '15 at 15:37
  • If you put correct controller identifier and correct storyboard name it should work, if not I don't know how to give more help. – theDC Jul 02 '15 at 16:14
  • I'd connected it to a gesture. I just pointed a button at the same code and it executed and gave me a rotating/flip transition. Not the desired slide from left, but I think the code executed! Not sure why it didn't work for a gesture. Also, after it ran my top nav bar disappeared!! – Dave G Jul 02 '15 at 16:20
  • It dissapeared because the destination probably is not connected to navigation controller, but I'm not sure. Great that the code executed. Good luck – theDC Jul 02 '15 at 16:23
4

Accepted answer updated for Swift 3 (as of Jun 2017)

Segue from left to right

import UIKit

class SegueFromLeft: UIStoryboardSegue {
    override func perform() {
        let src = self.source       //new enum
        let dst = self.destination  //new enum

        src.view.superview?.insertSubview(dst.view, aboveSubview: src.view)
        dst.view.transform = CGAffineTransform(translationX: -src.view.frame.size.width, y: 0) //Method call changed
        UIView.animate(withDuration: 0.25, delay: 0.0, options: UIViewAnimationOptions.curveEaseInOut, animations: { 
            dst.view.transform = CGAffineTransform(translationX: 0, y: 0)
        }) { (finished) in
            src.present(dst, animated: false, completion: nil) //Method call changed
        }
    }
}

Segue from right to left

import UIKit

class SegueFromRight: UIStoryboardSegue {
    override func perform() {
        let src = self.source
        let dst = self.destination

        src.view.superview?.insertSubview(dst.view, aboveSubview: src.view)
        dst.view.transform = CGAffineTransform(translationX: src.view.frame.size.width*2, y: 0) //Double the X-Axis
        UIView.animate(withDuration: 0.25, delay: 0.0, options: UIViewAnimationOptions.curveEaseInOut, animations: { 
            dst.view.transform = CGAffineTransform(translationX: 0, y: 0)
        }) { (finished) in
            src.present(dst, animated: false, completion: nil)
        }
    }
}
Anjan Biswas
  • 7,746
  • 5
  • 47
  • 77
2

It sounds like you are just trying to pop a view controller off the UINavigationController's stack, just like the default back button would.

You can do one of two things. The easiest is to connect your custom back button to an IBAction that calls popViewControllerAnimated():

@IBAction func tappedCustomBackButton(sender: AnyObject) {
    self.navigationController?.popViewControllerAnimated(true)
}

Or you can create an unwind segue from your second view controller back to your first.

Community
  • 1
  • 1
Richard Venable
  • 8,310
  • 3
  • 49
  • 52
  • This is my problem, I'm not trying to pop a view off the stack! That's why it's hard! Left me clarify: Think of it like a book, if I use the Index to hop to chapter 4, then swipe right and pop a view from the stack, I'll be back at the Index. But if you're on Chapter 4, page 394 and you swipe right, you don't want to go back to the index! You want to go to the last page of chapter 3, page 393! So the nav stack is no help to me. I already have the gesture and bottom nav pointing at page 393, so that is fine, but my problem is it confuses the user when the page slides in from the right. – Dave G Jun 12 '15 at 02:05
  • 1
    Sounds like you should use a UIPageViewController. That is what book readers, like iBooks, typically use for navigation. – Richard Venable Jun 12 '15 at 02:11
  • Richard would that mean I'd have to rebuild my pages? Right now they are a series of separate UIViewControllers. Thank you – Dave G Jun 12 '15 at 10:40
  • Each page should be its own view controller. You make some object conform to UIPageViewControllerDelegate and UIPageViewControllerDataSource which vends each page's view controller. If you want to jump to a new page, use setViewControllers:direction:animated:completion: to set the new visible pages. – Richard Venable Jun 15 '15 at 15:52
  • Thanks Richard, it sounds like that's the right root for me then. I'll look for a good tutorial on how the code should look. If you know of any and can post the link, that'd be great. Either way, thanks for the solution and once I give it a try I'll post the outcome. – Dave G Jun 16 '15 at 03:43
  • Hi Richard, my problem isn't solved! I found this tutorial (http://www.appcoda.com/uipageviewcontroller-storyboard-tutorial/) on implementing page view controllers and it seems like overkill. My app already presents all the views, in the order I would like. So implementing a this much code seems like a waste. Is there no simpler way to simply change the transition style they are currently being presented with? – Dave G Jun 24 '15 at 03:18
1

Segue from right.You can override the perform function in segue like below. put this function inside a custom segue class and assign this class to the segue. it will work for both controllers with navigation controller and without navigation controller

override func perform()
{
    let src = self.sourceViewController
    print(src)
    let dst = self.destinationViewController
    print(dst)

    src.view.superview?.insertSubview(dst.view, aboveSubview: src.view)
    dst.view.transform = CGAffineTransformMakeTranslation(src.view.frame.size.height, 0)

    UIView.animateWithDuration(0.35,
                               delay: 0.0,
                               options: UIViewAnimationOptions.CurveEaseInOut,
                               animations: {
                                dst.view.transform = CGAffineTransformMakeTranslation(0, 0)
        },
                               completion: { finished in
                                if let navController = src.navigationController {
                                    navController.pushViewController(dst, animated: false)

                                } else {
                                    src.presentViewController(dst, animated: false, completion: nil)
                                }            }
    )
}

if you want the segue from left then use this

CGAffineTransformMakeTranslation(-src.view.frame.size.height, 0)
Ashwin Felix
  • 213
  • 2
  • 6
1

In my case I used to make a sidebar menu...

I created a new view controller and two customs segues

TO OPEN THE MENU:

import Foundation
import UIKit

class SegueFromLeft: UIStoryboardSegue {

  override func perform() {

    let src = self.source as UIViewController
    let dst = self.destination as UIViewController

    src.view.superview?.insertSubview(dst.view, aboveSubview: src.view)
    dst.view.transform = CGAffineTransform(translationX: -src.view.frame.size.width, y: 0)

    UIView.animate(withDuration: 0.25,
                               delay: 0.0,
                               options: UIViewAnimationOptions.curveEaseInOut,
                               animations: {
                                dst.view.transform = CGAffineTransform(translationX: 0, y: 0)
    },
                               completion: { finished in
                                src.present(dst, animated: false, completion: nil)
    }
    )

}

}

TO CLOSE THE MENU:

import Foundation
import UIKit

class SegueFromRight: UIStoryboardSegue {

override func perform() {

    let src = self.source as UIViewController
    let dst = self.destination as UIViewController

    src.view.superview?.insertSubview(dst.view, belowSubview: src.view)
    src.view.transform = CGAffineTransform(translationX: 0, y: 0)

    UIView.animate(withDuration: 0.25,
                               delay: 0.0,
                               options: UIViewAnimationOptions.curveEaseInOut,
                               animations: {
                                src.view.transform = CGAffineTransform(translationX: -src.view.frame.size.width, y: 0)
    },
                               completion: { finished in
                                src.dismiss(animated: false, completion: nil)
    }
    )
}
}

I hope it helped you...

1

Updated the accepted answer to Swift 4:

class SegueFromLeft: UIStoryboardSegue
{ 
     override func perform(){

    let src = self.source
    let dst = self.destination

    src.view.superview?.insertSubview(dst.view, aboveSubview: src.view)
    dst.view.transform = CGAffineTransform(translationX: -src.view.frame.size.width, y: 0)

    UIView.animate(withDuration: 0.25,
                               delay: 0.0,
                               options: UIViewAnimationOptions.curveEaseInOut,
                               animations: {
                                dst.view.transform = CGAffineTransform(translationX: 0, y: 0)
    },
                               completion: { finished in
                                src.present(dst, animated: false, completion: nil)
    })
     }
}

Feel free to copy this into the accepted answer and delete this comment. (It's a straight update, nothing new.)

Ian Kohlert
  • 484
  • 1
  • 4
  • 15
1

A little late to the party I know... its self.view.window not self.window

0

Hey guy I have complete solution just copy and past this code written swift 3.

func menu(){
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let vc = storyboard.instantiateViewController(withIdentifier: "MyAccountViewController") as! MyAccountViewController

    let transition: CATransition = CATransition()
    let timeFunc : CAMediaTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
    transition.duration = 0.5
    transition.timingFunction = timeFunc
    transition.type = kCATransitionPush
    transition.subtype = kCATransitionFromLeft
    self.navigationController?.view.layer.add(transition, forKey: kCATransition)
    self.navigationController?.pushViewController(vc, animated: false)
}

Note: Change the name of your ViewController with "MyAccountViewController" text.

MRizwan33
  • 2,723
  • 6
  • 31
  • 42
0

To create a "back button" like animation, use this.

class SegueRightToLeft: UIStoryboardSegue {

override func perform() {
    let src = self.source       //new enum
    let dst = self.destination  //new enum

    src.view.superview?.insertSubview(dst.view, aboveSubview: src.view)
    dst.view.transform = CGAffineTransform(translationX: -src.view.frame.size.width/2, y: 0) 
    //slice the x axis translation in half

    UIView.animate(withDuration: 0.25, delay: 0.0, options: UIViewAnimationOptions.curveEaseInOut, animations: {
        dst.view.transform = CGAffineTransform(translationX: 0, y: 0)
    }) { (finished) in
        src.present(dst, animated: false, completion: nil)
    }
}
Felecia Genet
  • 367
  • 4
  • 3