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.
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 am presenting a navigation controller modally, with a table view controller as the root, and pushing more view controllers when the user taps a cell. Now, I want to apply a blur/vibrancy effect to the whole thing.
The storyboard looks like this:
Now, want to apply a blur effect to the whole navigation controller, so that the image in the initial view controller can be seen underneath.
I have somewhat succeeded by applying the same blur effect to both the table view controller and the detail view controller, like this:
Table View Controller
class TableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.tableFooterView = UIView(frame: .zero)
tableView.backgroundColor = UIColor.clear
let blurEffect = UIBlurEffect(style: .dark)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
tableView.backgroundView = blurEffectView
tableView.separatorEffect = UIVibrancyEffect(blurEffect: blurEffect)
}
}
Detail View Controller
class DetailViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .clear
let blurEffect = UIBlurEffect(style: .dark)
let blurView = UIVisualEffectView(effect: blurEffect)
blurView.translatesAutoresizingMaskIntoConstraints = false
view.insertSubview(blurView, at: 0)
NSLayoutConstraint.activate([
blurView.heightAnchor.constraint(equalTo: view.heightAnchor),
blurView.widthAnchor.constraint(equalTo: view.widthAnchor),
])
}
}
(I also set the modal presentation style of the navigation to .overFullScreen).
However, at the end of the navigation transition to the detail view controller, an annoying artifact can be seen in the blurred background:
I think this has to do with how, since iOS 7, the pushed view controller can not have a transparent background, otherwise the pushing one can be seen as it is instantly removed at the end of the transition.
I tried applying the blur effect to the navigation controller, and make both children's views transparent instead, but it doesn't look good either.
What is the proper way to apply a background blur/vibrancy effect to the contents of a navigation controller, seamlessly?
Try below steps. I haven't tried it but it should work.
Add background Image to window into didFinishLaunchingWithOptions and hide it by default.
Add same image into presenting modal view controller.
Make sure to clear background of your view controller.
So when you present it will show that image which is into modal view controller.
After complition Unhide window image and hide view controller image.
When you tap on back button to dismiss view, first unhide your view controller image and hide your window image. So your animation will look normal.
Let me know if you need any help.
As I figured out in the question, the push/pop animated transitions introduced since iOS 7 (where the view controller that is lower in the navigation stack "slides out at half the speed", so that it underlaps the one being pushed/popped) make it impossible to transition between transparent view controllers without artifacts.
So I decided to adopt the protocol UINavigationControllerDelegate, more precisely the method: navigationController(_:animationControllerFor:from:to:), essentially replicating the animated transitions of UINavigationController from scratch.
This allows me to add a mask view to the view that is transitioned away from during the push operation so that the underlapping section is clipped, and no visible artifacts occur.
So far I have only implemented the push operation (eventually, I'll have to do the pop operation too, and also the interactive transition form swipe gesture-based pop), I have made a custom UINavigationController subclass, and put it on GitHub.
(Currently, it supports animated push and pop, but not interactivity. Also, I haven't figured how to replicate the navigation bar title's "slide" animation yet - it just cross dissolves.)
It boils down to the following steps:
Initially, transform the .to view to the right, off-screen. During the animation, gradually transform it back to identity (center of screen).
Set a white UIView as mask to the .from view, initially with bounds equal to the .from view (no masking). During the animation, gradually reduce the width of the mask's frame property to half its initial value.
During the animation, gradually translate the from view halfway offscreen, to the left.
In code:
class AnimationController: NSObject, UIViewControllerAnimatedTransitioning {
// Super slow for debugging:
let duration: TimeInterval = 1
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromView = transitionContext.view(forKey: .from) else {
return
}
guard let toView = transitionContext.view(forKey: .to) else {
return
}
guard let toViewController = transitionContext.viewController(forKey: .to) else {
return
}
//
// (Code below assumes navigation PUSH operation. For POP,
// use similar code but with view roles and direction
// reversed)
//
// Add target view to container:
transitionContext.containerView.addSubview(toView)
// Set tagret view frame, centered on screen
let toViewFinalFrame = transitionContext.finalFrame(for: toViewController)
toView.frame = toViewFinalFrame
let containerBounds = transitionContext.containerView.bounds
toView.center = CGPoint(x: containerBounds.midX, y: containerBounds.midY)
// But translate it to fully the RIGHT, for now:
toView.transform = CGAffineTransform(translationX: containerBounds.width, y: 0)
// We will slide the source view out, to the LEFT, by half as much:
let fromTransform = CGAffineTransform(translationX: -0.5*containerBounds.width, y: 0)
// Apply a white UIView as mask to the source view:
let maskView = UIView(frame: CGRect(origin: .zero, size: fromView.frame.size))
maskView.backgroundColor = .white
fromView.mask = maskView
// The mask will shrink to half the width during the animation:
let maskViewNewFrame = CGRect(origin: .zero, size: CGSize(width: 0.5*fromView.frame.width, height: fromView.frame.height))
// Animate:
UIView.animate(withDuration: duration, delay: 0, options: [.curveEaseOut], animations: {
fromView.transform = fromTransform // off-screen to the left (half)
toView.transform = .identity // Back on screen, fully centered
maskView.frame = maskViewNewFrame // Mask to half width
}, completion: {(completed) in
// Remove mask, or funny things will happen to a
// table view or scroll view if the user
// "rubber-bands":
fromView.mask = nil
transitionContext.completeTransition(completed)
})
}
The result:
(I added a text view to the detail view controller for clarity)
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)
}
}
I added two view controllers to my scroll view, and have animations on both view controllers, but those animations should only be called when the vc is showing. Instead both animations are being executed when the first vc is showing.
func setUpScrollView() {
for i in 0..<2 {
let vc = NewViewController()
self.addChildViewController(vc)
scrollView.addSubview(vc.view)
vc.didMove(toParentViewController: self)
var frame: CGRect = vc.view.frame
frame.origin.x = self.view.frame.width * CGFloat(i)
vc.view.frame = frame
self.scrollView.contentSize = CGSize(width: self.view.frame.width * CGFloat(2), height: self.view.frame.size.height)
}
}
NewViewController
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("calling animation") //this is called twice immediatley after for loop, but only shows one vc.
}
UIScrollView doesn't manage content loading the way a UITableView does, so whatever you put in the scroll view's content area will fire its viewDidAppear immediately whether it is in the visible area or not.
To decide when to start your animation, you're going to have to manually keep track of what is actually visible in the scroll view's content area:
Assign a UIScrollViewDelegate and do the check in its scrollViewDidScroll(), which is called every frame that the scrollView is scrolling.
You can use scrollView.contentOffset and scrollView.bounds along with the size and coordinates of the subview you're interested in to determine visibility, then trigger your animation when it first becomes visible.
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