Currently I try to work on a custom segue it should look like:
The [Destination View] should be behind the [Source View], this [Source View] should animate from 100% down to 0.1% and then remove, in the animation time the [Destination View] should also be there in background.
So you see the [Source View] become small in front of the [Destination View] and get removed.
This is my code:
import UIKit
class CustomSegueFromBigtoSmall: UIStoryboardSegue {
override func perform() {
let sourceVC = self.sourceViewController
let destinationVC = self.destinationViewController
sourceVC.view.addSubview(destinationVC.view)
UIView.animateWithDuration(0.5, delay: 0.0, options: .CurveEaseInOut, animations: { () -> Void in
sourceVC.view.transform = CGAffineTransformMakeScale(0.1, 0.1)
}){ (finished) -> Void in
destinationVC.view.removeFromSuperview()
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(0.001 * Double(NSEC_PER_SEC)))
dispatch_after(time, dispatch_get_main_queue(), { () -> Void in
sourceVC.presentViewController(destinationVC, animated: false, completion: nil)
})
}
}
}
right now I see my [Source View], that instantly becomes my [Destination View]. The [Destination View] become small in front of a black background. Once it is "small" it simply appears as full screen.
The way I would do this in iOS 9 is to make this a presentation (modal) segue with a custom segue implementation. Your custom segue then simply calls super.perform() to do the actual presentation. But first, the segue sets itself as the destination's transitionDelegate and sets its presentation style to Custom. Now you're just doing an ordinary custom transition animation, with your own transitioning delegate and your own UIPresentationController, and you can do whatever you want, in good order.
Related
I have looked at several examples and it looks like this used to be the default effect for a Push segue which is now deprecated. The new default animation for a Show or Present Modally segue is to have the new view slide in from the bottom to the top.
I am using the code below after looking through several examples of how to create your own custom segue. I am getting the effect that I want which is to have the new view slide in from the right to the left right until the end of the animation. Right at the end, when the new view is fully displayed on the screen, there is a fort of blink, where for a few seconds I see the old view and then the new view is presented again.
Please help! Thanks.
class CustomStoryboardSegue: UIStoryboardSegue {
override func perform() {
let sourceVC = self.sourceViewController
let destinationVC = self.destinationViewController
sourceVC.view.addSubview(destinationVC.view)
destinationVC.view.transform = CGAffineTransformMakeTranslation( sourceVC.view.frame.size.width, 0)
UIView.animateWithDuration(0.25, delay: 0.0, options: UIViewAnimationOptions.CurveEaseInOut, animations: { () -> Void in
destinationVC.view.transform = CGAffineTransformMakeTranslation(1.0, 1.0)
}) { (finished) -> Void in
destinationVC.view.removeFromSuperview()
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(0.001 * Double(NSEC_PER_SEC)))
dispatch_after(time, dispatch_get_main_queue()) {
sourceVC.presentViewController(destinationVC, animated: false, completion: nil)
}
}
}
}
UPDATE
To clarify the finished block in my code above, I delay the presentation of the view controller by a second to avoid getting the
error: Unbalanced calls to begin/end appearance transitions for TableViewController
because I cannot remove the source view and present the destination view at the exact same time.
To test this I removed the delay and updated my finished block as follows and now I am getting the 'unbalanced call error' as expected but, interestingly enough, I am also seeing the blink at the end of the transition.
}) { (finished) -> Void in
destinationVC.view.removeFromSuperview()
sourceVC.presentViewController(destinationVC, animated: false, completion: nil)
}
In my UIStoryboardSegue subclass, I use presentViewController at the end of the perform method, this cause viewDidAppear/viewWillAppear to be called twice?
How can I prevent this?
Thanks
Current code:
override func perform() {
// Assign the source and destination views to local variables
let sourceView = sourceViewController.view as UIView!
let destView = 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
destView.frame = CGRectMake(0.0, screenHeight, screenWidth, screenHeight)
// Add the destination view to the window
let window = UIApplication.sharedApplication().keyWindow
window?.insertSubview(destView, aboveSubview: sourceView)
// Animate the transition
UIView.animateWithDuration(0.7, animations: { () -> Void in
// Scale down the source view
sourceView.transform = CGAffineTransformScale(sourceView.transform, 0.90, 0.90)
}) { (Finished) -> Void in
}
UIView.animateWithDuration(1.0, delay: 0.2, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.7, options: .CurveEaseOut, animations: { () -> Void in
destView.frame = CGRectOffset(destView.frame, 0.0, -screenHeight)
}) { (finished) -> Void in
self.sourceViewController.presentViewController(self.destinationViewController as UIViewController, animated: false, completion: nil)
}
}
I see now that without presentViewController the new controller is destroyed soon after the animation custom segue. I see why we need it.
The reason why viewDidAppear gets called twice is that you first insert the viewcontroller as a subview into the keyWindow. (which triggers the viewDidAppear)
and then present the viewcontroller. (which triggers the viewDidAppear also)
This brings also some side effects to your app because you will end up with a viewHierachy like:
-- KeyWindow
---- FirstVC View
---- SecondVC View
---- UITransitionView (because of the presentVC)
------- SecondVC View
If you want to achieve this swipe/scroll transition why not just use a scrollView and have childviewcontrollers in it?
The other solution for this would be to use a custom transition and simply call presentViewController with a custom transition as described here: http://www.raywenderlich.com/113845/ios-animation-tutorial-custom-view-controller-presentation-transitions
I am building an app and recently discovered a huge memory leak caused by traditional segues.
Therefore I learned about unwind segue. Everything works just fine if I simply use:
#IBAction func prepareForUnwindToMainFromFriends(segue: UIStoryboardSegue) {
}
Memory leak is solved and 'everything is awesome'. But this solution looks ugly on a UI point of view. So I implemented this function from this website. And changed it a little.
override func segueForUnwindingToViewController(toViewController: UIViewController, fromViewController: UIViewController, identifier: String?) -> UIStoryboardSegue {
return UIStoryboardSegue(identifier: identifier, source: fromViewController, destination: toViewController) {
let fromView = fromViewController.view
let toView = toViewController.view
if let containerView = fromView.superview {
let initialFrame = fromView.frame
var offscreenRect = initialFrame
var offscreenRectFinal = initialFrame
offscreenRect.origin.x += CGRectGetWidth(initialFrame)
offscreenRectFinal.origin.x -= CGRectGetWidth(initialFrame)
toView.frame = offscreenRect
containerView.addSubview(toView)
let duration: NSTimeInterval = 1.0
let delay: NSTimeInterval = 0.0
let options = UIViewAnimationOptions.CurveEaseInOut
let damping: CGFloat = 0.9
let velocity: CGFloat = 4.0
UIView.animateWithDuration(duration, delay: delay, usingSpringWithDamping: damping,
initialSpringVelocity: velocity, options: options, animations: {
toView.frame = initialFrame
fromView.frame = offscreenRectFinal
}, completion: { finished in
fromView.removeFromSuperview()
if let navController = toViewController.navigationController {
navController.popToViewController(toViewController, animated: false)
}
})
}
}
}
But now I get an error message:
2015-05-12 08:47:31.841 PING0.4[4343:1308313] Warning: Attempt to present <NotificationViewController: 0x177030b0> on <PING0_4.ViewController: 0x16271000> which is already presenting <NotificationViewController: 0x1a488170>
And I am blocked in my app. I can go from VC1 to VC2, then back to VC2 but then I cannot get back to VC1 again. It looks like I can only use this segue once.
Any one has any idea of what is going on?
Created Sample code for unwind segue with above transition animation code. Checkout SampleUnwind project that will help you to understand unwind segue(and how simple it is).
In project there is one navigation controller and inside it there are three view controller (Home->First->second).
In Home controller following unwind action is created, which will be called when 'Home' button of second controller is tapped(simple unwind stuff).
#IBAction func unwindToHomeViewController(segue:UIStoryboardSegue) {
}
I have created TempNavigationController subclassing UINavigationController and set that TempNavigationController to navigation controller in storyboard.
Above method you given is pesent inside it as this will be container of fromViewController as per following referance.
Reference: Apple documentation about Transitioning Between Two Child View Controllers.
You can compare this with your project and maybe you can find any duplicate(or multiple/wrong) segue in your project.
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)
}