This question already has answers here:
Navigation controller custom transition animation
(4 answers)
Closed 5 years ago.
i'm having a navigationcontroller that contains a button. when this button is pressed i would like to slide in new view from the right, with the current sliding out left on the same time. Exactly like if u have horizontal scrolling scrollview. Default NavigationController animation seem to just push the new view on top of the other. is there anyway to easily achieve this ?
Implement a custom segue between first and second view controllers and do this in perform method
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 , 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)
secondVCView.frame = CGRectOffset(secondVCView.frame,-screenWidth, 0)
}) { (Finished) -> Void in
self.sourceViewController.presentViewController(self.destinationViewController as UIViewController,
animated: false,
completion: nil)
}
}
Related
As an exercise I would like to not use a navigation controller.
I have the following project:
The 2 push segues are triggered by each button in the center of the ViewControllers. They use the following custom class:
class CustomSegue: UIStoryboardSegue {
override func perform() {
weak var firstView = self.source.view as UIView?
weak var secondView = self.destination.view as UIView?
let screenSize = UIScreen.main.bounds.size
secondView?.frame = CGRect(x: screenSize.width, y: 0.0, width: screenSize.width, height: screenSize.height)
UIApplication.shared.keyWindow?.insertSubview(secondView!, aboveSubview: firstView!)
UIView.animate(withDuration: 0.3, animations: { () -> Void in
firstView!.frame = firstView!.frame.offsetBy(dx: -1 * screenSize.width, dy: 0.0)
secondView!.frame = secondView!.frame.offsetBy(dx: -1 * screenSize.width, dy: 0.0)
}) { (_) -> Void in
self.source.present(self.destination, animated: false, completion: nil)
}
}
}
This goes extremely wrong because it creates a new ViewController object at each segue performed. The memory usage keeps going up and never down as UIApplication.shared.keyWindow.subviews is getting filled with UIViews...
The project I am working on has several ViewControllers which can call any other randomly. For this reason I didn't succeed to use UIViewController.dismiss(animated:completion:) because it systematically makes it go back to the previous ViewController.
How can I definitely remove the previous ViewController after having performed a segue?
Based on the comments, you don't want traditional "Navigation Controller" features - mainly, you don't need a "Back" button.
So, one option would be to use Child View Controllers.
Your "main" view controller would have nothing but a "container" view. On startup, you load your first VC as a Child VC, and add its view as a subview of the container. When you want to "navigate" to any other VC, load that VC as a Child VC, replace the current view in the container with the new ChildVC's view, and unload the current Child VC.
I have a storyboard segue, and while showing the new view, I want an UIView to always stay on top, so the segue does not affect it. Tried animating insertSubview, but it does not have the push from bottom animation.
Here is some code I quickly whipped up showing two view controllers, the one you are going to and one from. I put the animation in a function called buttonPressed, but it should go whatever function that calls the transition. Rest of the code is pretty self explanatory.
For this to work both view controllers will need a view with same name (or different names but keep track of which is which), I use IBOutlet staticView. And then in interface builder make sure they have the same constraints or frame so that when you set on vc's static view to another it sticks to same spot.
class ViewControllerFrom: UIViewController {
#IBOutlet weak var staticView: UIView!
#IBAction func buttonPressed(_ sender: Any) {
let toVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "toVC") as! ViewControllerTo
//Add nextVC's view to ours as subview
self.view.addSubview(toVC.view)
//Set its starting height to be below current view.
toVC.view.frame = CGRect(x: 0, y: view.frame.height, width: view.frame.width, height: view.frame.height)
//This is important to make sure staticView stays in front of view animating in
view.bringSubview(toFront: staticView)
if toVC.staticView != nil {
toVC.staticView.isHidden = true
}
//I use animation duration 0.4, close to default animations by iOS.
UIView.animate(withDuration: 0.4, animations: {
toVC.view.frame.origin.y = 0
}, completion: { (success) in
if success {
//Now that view is in place, set static view from old vc to new vc and reshow it. Then do the actual presentation unanimated.
toVC.staticView.isHidden = false
toVC.staticView = self.staticView
self.present(toVC, animated: false, completion: nil)
}
})
}
}
class ViewControllerTo : UIViewController {
#IBOutlet weak var staticView: UIView!
}
I'm using a custom segue in order to transition between two view controllers. This is the segue I'm using for one of those transitions:
class SegueFromRight: UIStoryboardSegue{
override func perform() {
// Assign the source and destination views to local variables.
let src = self.source.view as UIView!
let dst = self.destination.view as UIView!
// Get the screen width and height.
let screenWidth = UIScreen.main.bounds.size.width
let screenHeight = UIScreen.main.bounds.size.height
// Specify the initial position of the destination view.
dst?.frame = CGRect(0.0, screenHeight, screenWidth, screenHeight)
// Access the app's key window and insert the destination view above the current (source) one.
let window = UIApplication.shared.keyWindow
window?.insertSubview(dst!, aboveSubview: src!)
// Animate the transition.
UIView.animate(withDuration: 0.4, animations: { () -> Void in
src?.frame = (src?.frame.offsetBy(dx: -screenWidth, dy: 0.0))!
dst?.frame = (dst?.frame.offsetBy(dx: -screenWidth, dy: 0.0))!
}) { (Finished) -> Void in
self.source.present(self.destination as UIViewController,
animated: false,
completion: nil)
}
}
}
The segue works perfectly, however the animation is not how I'd like it. When it "pushes" the destination view controller, it's shown black. It only turns to the actual view controller as soon as the animation is finished. I guess that's because the new view controller is only loaded once the animation is finished. But how would I go about preloading the new view controller, so that the animation looks smooth?
The unwinding segue does not display that black animation, as the destination view controller (the previous source view controller) is already loaded of course.
This is likely your problem dst?.frame = CGRect(0.0, screenHeight, screenWidth, screenHeight).
Try this instead dst?.frame = CGRect(screenWidth, 0.0, screenWidth, screenHeight).
Your original line sets the destination view to be off the bottom of the screen rather than off to the right edge of the screen. When you animate the views by the screen width the destination slides from directly below the screen to below and to the left.
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 have a tab bar controller and 3 childs of it, also I have another view which I made a custom segue from the childs to the view controller, and also a custom unwind segue from the view controller to the child. The problem is that while the unwind is happening, the tab bar is hidden and it shows when the unwind is finished.
HereĀ“s a GIF example:
Here's my code of the custom segue unwind:
import UIKit
class AddMeCustomSegueUnwind: 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 screenHeight = UIScreen.mainScreen().bounds.size.height
let window = UIApplication.sharedApplication().keyWindow
//window?.insertSubview(firstVCView, aboveSubview: secondVCView)
//window?.insertSubview(firstVCView, belowSubview: secondVCView)
window?.insertSubview(firstVCView, atIndex: 0)
// Animate the transition.
UIView.animateWithDuration(0.4, animations: { () -> Void in
secondVCView.frame = CGRectOffset(secondVCView.frame, 0.0, -screenHeight + 64)
}) { (Finished) -> Void in
self.sourceViewController.dismissViewControllerAnimated(false, completion: nil)
}
}
}
To make the unwind work as I said, the unwind segue needed to be done from the tab bar controller so I created a UITabBarController and add the same unwind to it.