45
votes

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

12
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
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

12 Answers

92
votes

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'

43
votes

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.

11
votes

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)
            }
        )
    }
}
4
votes

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/

4
votes

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)
        }
    }
}
2
votes

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.

1
votes

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
votes

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)
1
votes

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.)

1
votes

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

0
votes

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.

0
votes

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)
    }
}