I have put a UINavigationController into an expandable "Drawer".
My goal is to let each viewController in the navigation stack to have its own "preferred" height.
Let's say VC1 needs to be tall. When navigating back to VC1 from VC2 I want it to animate its height to be tall. The animation logic seems to be working, even with the interaction of swipe.
But for some reason, the viewControllers in the navigationController are "cut off". Their constraints are correct, but they aren't updated(?). Or a portion of the content simply won't render, until I touch the view again. The invisible area on the bottom will even accept touches.
Take a look:
The expected result is that the contentViewControllers (first and second) always extend to the bottom of the screen. They are constraint to do this, so the issue is that they won't "render"(?) during the transition.
In the UINavigationController's delegates, I do the following:
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
transitionCoordinator?.animate(alongsideTransition: { (context) in
drawer.heightConstraint.constant = targetHeight
drawer.superview?.layoutIfNeeded()
}, completion: { (context) in
print("done")
})
}
The height change is perfect. But the content in the navigation won't comply. The navigationController is constrained to leading, trailing, bottom, and a stored heightConstraint that changes its constant.
As soon as I touch/drag the navigationController/content it instantly "renders the unrendered", and everything is fine. Why is this happening?
When inspecting the view hierarchy, it looks like this:
The NavigationController is as tall as it needs to be, but the content is the same height as the entire Drawer was when the transition started, and it doesn't update until I touch it.
Why?
Edit: I've pushed the code to my GitHub if you want to take a look. Beware though, there are several other issues there as well (animation etc.), don't mind them. I only want to know why the navigation won't render "future" heights.
You can solve the above problem by having a custom animationController for your navigationController and setting the appropriate frame for the destinationView in animateTransition function of your custom UIViewControllerAnimatedTransitioning implementation. The result would be like the one in the following gif image.
Your custom UIViewControllerAnimatedTransitioning may look like the one below.
final class TransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let presenting: Bool
init(presenting: Bool) {
self.presenting = presenting
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return TimeInterval(UINavigationController.hideShowBarDuration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromView = transitionContext.view(forKey: .from) else { return }
guard let toView = transitionContext.view(forKey: .to) else { return }
let duration = transitionDuration(using: transitionContext)
let container = transitionContext.containerView
if presenting {
container.addSubview(toView)
} else {
container.insertSubview(toView, belowSubview: fromView)
}
let toViewFrame = toView.frame
toView.frame = CGRect(x: presenting ? toView.frame.width : -toView.frame.width, y: toView.frame.origin.y, width: toView.frame.width, height: toView.frame.height)
UIView.animate(withDuration: duration, animations: {
toView.frame = toViewFrame
fromView.frame = CGRect(x: self.presenting ? -fromView.frame.width : fromView.frame.width, y: fromView.frame.origin.y, width: fromView.frame.width, height: fromView.frame.height)
}) { (finished) in
container.addSubview(toView)
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
And, in your navigation controller provide custom animationController as follows.
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
switch operation {
case .push:
return TransitionAnimator(presenting: true)
case .pop:
return TransitionAnimator(presenting: false)
default:
return nil
}
}
PS :- I've also given a pull request in your github repo.
You can update controllers height yourself:
first you need to keep a reference to controllers:
class ContainedNavigationController: UINavigationController, Contained, UINavigationControllerDelegate {
private var controllers = [UIViewController]()
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { controllers = viewControllers }
/* Rest of the class */
Then you can update their heights accordingly. Don't forget to add new controller.
private func updateHeights(to height: CGFloat, willShow controller: UIViewController) {
controller.view.frame.size.height = height
_ = controllers.map { $0.view.frame.size.height = height }
}
You can use it in your code like this:
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
if let superHeight = view.superview?.superview?.bounds.size.height, let drawer = (view.superview as? Drawer), let targetHeight = (viewController as? Contained)?.currentNotch.height(availableHeight: superHeight){
transitionCoordinator?.animate(alongsideTransition: { (context) in
drawer.heightConstraint.constant = targetHeight
drawer.superview?.layoutIfNeeded()
self.updateHeights(to: targetHeight, willShow: viewController) // <- Here
}, completion: { (context) in
self.updateHeights(to: targetHeight, willShow: viewController) // <- Here
})
}
}
Result:
Maybe this is not the cleanest code can done, but I just want to solve the issue and giving you the idea
Update
That shadow you have seen so far when you drag from the edge is a view called UIParallaxDimmingView. I have added a fix for that size too. So no more visual issues:
private func updateHeights(to height: CGFloat, willShow controller: UIViewController) {
controller.view.frame.size.height = height
_ = controllers.map { $0.view.frame.size.height = height }
guard controllers.contains(controller) else { return }
_ = controller.view.superview?.superview?.subviews.map {
guard "_UIParallaxDimmingView" == String(describing: type(of: $0)) else { return }
$0.frame.size.height = height
}
}
I have added a pull request from my fork.
You can to set size like bellow:
refrence -> https://github.com/satishVekariya/Drawer/tree/boom-diggy-boom
class FirstViewController: UIViewController, Contained {
// your code
override func updateViewConstraints() {
super.updateViewConstraints()
if let parentView = parent?.view {
view.frame.size.height = parentView.frame.height
}
}
}
Your navigation vc:
class ContainedNavigationController:UINavigationController, Contained, UINavigationControllerDelegate{
///Your code
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
viewController.updateViewConstraints()
// Your code start
// end
}
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
viewController.updateViewConstraints()
}
}
You can set height constraint and call layoutIfNeeded method without transition block.
Below is the code snippet for the same:
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
if let superHeight = view.superview?.superview?.bounds.size.height, let drawer = (view.superview as? Drawer), let targetHeight = (viewController as? Contained)?.currentNotch.height(availableHeight: superHeight){
drawer.heightConstraint.constant = targetHeight
drawer.layoutIfNeeded()
}
}
#sti Please let me know if it helps, please do up vote for the same
Related
I want all of the functionality of a pageSheet UIModalPresentationStyle segue but I only want the presented ViewController to show half the screen (see the example in the image below).
I am presenting it modally using the pageSheet modalPresentationStyle but it always presents it at 100% height.
I haven't been able to figure out how to limit or modify a ViewController's height. I tried the following in my SecondViewController but it didn't work:
import UIKit
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.preferredContentSize = CGSize(width: self.view.frame.width, height: 400)
}
}
I'm initiating the segue with Storyboard Segues, and a button that presents it modally:
I figured out a way to do it, which I find to be pretty simple:
import UIKit
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let newView = UIView(frame: CGRect(x: 0, y: 500, width: self.view.frame.width, height: 400))
newView.backgroundColor = .yellow
newView.layer.cornerRadius = 20
self.view = UIView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height))
// self.view is now a transparent view, so now I add newView to it and can size it however, I like.
self.view.addSubview(newView)
// works without the tap gesture just fine (only dragging), but I also wanted to be able to tap anywhere and dismiss it, so I added the gesture below
let tap = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
self.view.addGestureRecognizer(tap)
}
#objc func handleTap(_ sender: UITapGestureRecognizer? = nil) {
dismiss(animated: true, completion: nil)
}
}
In order to achieve you will need to subclass UIPresentationController and implement the protocol UIViewControllerTransitioningDelegate in the presenting controller and set transitioningDelegate and modalPresentationStyle of presented view controller as self(presenting view controller) and .custom respectively. Implement an optional function of UIViewControllerTransitioningDelegate:
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source _: UIViewController) -> UIPresentationController?
and return the custom presentationController which sets the height of presented controller as per your requirement.
Basic code that might help:
class CustomPresentationController: UIPresentationController {
var presentedViewHeight: CGFloat
init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, presentedViewHeight: CGFloat) {
self.presentedViewHeight = presentedViewHeight
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
override var frameOfPresentedViewInContainerView: CGRect {
var frame: CGRect = .zero
frame.size = CGSize(width: containerView!.bounds.width, height: presentedViewHeight)
frame.origin.y = containerView!.frame.height - presentedViewHeight
return frame
}
}
Implementation of optional function:
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source _: UIViewController) -> UIPresentationController? {
let presentationController = CustomPresentationController(presentedViewController: presented, presenting: presenting, presentedViewHeight: 100)
return presentationController
}
You can also play with other optional functions and adding some other functionalities to CustomPresentationController like adding blur background, adding tap functionality and swipe gesture.
We can add our view in UIActivityController and remove UIActivityController's default view and if you add navigation controller so you will get navigation also, so you can do half your controller by this:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func actionPresent(_ sender: UIBarButtonItem) {
let vc1 = storyboard?.instantiateViewController(withIdentifier: "ViewControllerCopy")
let vc = ActivityViewController(controller: vc1!)
self.present(vc, animated: true, completion: nil)
}
}
class ActivityViewController: UIActivityViewController {
private let controller: UIViewController!
required init(controller: UIViewController) {
self.controller = controller
super.init(activityItems: [], applicationActivities: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
let subViews = self.view.subviews
for view in subViews {
view.removeFromSuperview()
}
self.addChild(controller)
self.view.addSubview(controller.view)
}
}
for example you can check this repo:
https://github.com/SomuYadav/HalfViewControllerTransition
First off, this is my view controller/segue setup:
The three rightmost view controllers' background views are UIVisualEffectViews through which the source view controllers should be visible. They were added in the various viewDidLoad()s like this:
let blurEffect = UIBlurEffect(style: .dark)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
blurEffectView.frame = self.view.bounds
blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.tableView.backgroundView = blurEffectView
Now, the main view controller (the "Garbage Day" one) is visible through the settings view controller, but the settings VC disappears whenever one of the two rightmost VCs is fully on screen. Here's a screen recording:
Screen recording of the source view controller dis- and reappearing
(Please ignore the glitches, the app I used to upload this apparently corrupted the video)
I get that technically, the Show segue doesn't have the "Over Current Context" thingy and therefore, I shouldn't expect the source VC to not disappear, but there has to be a way to make this work without custom segues.
I suggest you create a custom transition between view controllers.
I just wrote and tested this class:
class AnimationController: NSObject, UIViewControllerAnimatedTransitioning
{
var pushing = true
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let duration = transitionDuration(using: transitionContext)
let toVc = transitionContext.viewController(forKey: .to)!
let toView = transitionContext.view(forKey: .to)!
let fromView = transitionContext.view(forKey: .from)!
let container = transitionContext.containerView
if pushing {
container.addSubview(fromView)
container.addSubview(toView)
}
var finalFrame = transitionContext.finalFrame(for: toVc)
if pushing {
finalFrame.origin.x = finalFrame.width
toView.frame = finalFrame
finalFrame.origin.x = 0
} else {
finalFrame.origin.x = finalFrame.width
}
UIView.animate(withDuration: duration, delay: 0, options: .curveEaseOut, animations: {
if self.pushing {
toView.frame = finalFrame
} else {
fromView.frame = finalFrame
}
}) { (_) in
transitionContext.completeTransition(true)
if self.pushing {
container.insertSubview(fromView, belowSubview: toView)
} else {
fromView.removeFromSuperview()
}
}
}
}
In your UINavigationController class do the following:
class NavigationController: UINavigationController {
let animationController = AnimationController()
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
}
And this extension:
extension NavigationController: UINavigationControllerDelegate
{
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
animationController.pushing = operation == .push
return animationController
}
}
However, this makes you lose the interactive dismiss gesture (Swiping from the left of the screen to dismiss) So you would need to fix that yourself.
I have a navigation controller, and inside of that navigation controller I have a home screen, from the home screen I click a button which goes to another screen.
But the standard show animation when using a navigation controller is that it slides from the side, but what I want to do is that the view controller slides up from bottom of the screen and creates a sort of bouncing animation when it reaches the top.
Anyone who wanted to use custom transition two things to remember UIViewControllerAnimatedTransitioning and UIViewControllerTransitioningDelegate protocols. Now conform UIViewControllerAnimatedTransitioning inside your customclass inheriting from NSObject
import UIKit
class CustomPushAnimation: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.2
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerVw = transitionContext.containerView
let fromViewController = transitionContext.viewController(forKey: .from)
let toViewController = transitionContext.viewController(forKey: .to)
guard let fromVc = fromViewController, let toVc = toViewController else { return }
let finalFrame = transitionContext.finalFrame(for: toVc)
//For different animation you can play around this line by changing frame
toVc.view.frame = finalFrame.offsetBy(dx: 0, dy: finalFrame.size.height/2)
containerVw.addSubview(toVc.view)
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
toVc.view.frame = finalFrame
fromVc.view.alpha = 0.5
}, completion: {(finished) in
transitionContext.completeTransition(finished)
fromVc.view.alpha = 1.0
})
}
}
The above method will take care of the animation. After that create
the above object and use inside yourViewController class
import UIKit
class YourViewController: UIViewController {
lazy var customPushAnimation: CustomPushAnimation = {
return CustomPushAnimation()
}()
func openViewControler() {
}let vc =//Assuming your view controller which you want to open
let navigationController = UINavigationController(rootViewController: vc)
//Set transitioningDelegate to invoke protocol method
navigationController.transitioningDelegate = self
present(navigationController, animated: true, completion: nil)
}
Note: In order to see the animation. Never set animation flag to false
while presenting the ViewController. Otherwise your animation will
never work.
Lastly implement the UIViewControllerTransitioningDelegate protocol method inside YourViewcontroller class
extension YourViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return customPushAnimation
}
Whenever you present the viewcontroller above protocol method will
called and your animation magic will appear.
I'm working with a few standard segues in storyboard and they each have the same background color. The issue I'm having is that when the segue transition nears completion there appears a dark shadow like background around the whole frame.
It's very faint, but enough to cause an issue. Has anyone come across this before?
The standard navigation controller push/pop animations darken the view that you're pushing from and the one you're popping to. If you don't like that, you can customize the transition, using an animation that just slides views in and out, but does no dimming of anything:
// this is the view controller you are pushing from
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.delegate = self
}
}
// make the view controller conform to `UINavigationControllerDelegate`
extension ViewController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return PushPopAnimator(operation: operation)
}
}
// The animation controller
class PushPopAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let operation: UINavigationControllerOperation
init(operation: UINavigationControllerOperation) {
self.operation = operation
super.init()
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let from = transitionContext.viewController(forKey: .from)!
let to = transitionContext.viewController(forKey: .to)!
let rightTransform = CGAffineTransform(translationX: transitionContext.containerView.bounds.size.width, y: 0)
if operation == .push {
to.view.transform = rightTransform
transitionContext.containerView.addSubview(to.view)
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
to.view.transform = .identity
}, completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
} else if operation == .pop {
to.view.transform = .identity
transitionContext.containerView.insertSubview(to.view, belowSubview: from.view)
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
from.view.transform = rightTransform
}, completion: { finished in
from.view.transform = .identity
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
}
For information on custom transitions with view controllers, see WWDC 2013 video Custom Transitions Using View Controllers.
I encountered a strange bug. I am just using iOS's custom transitioning method for UIViewControllers using UIViewControllerTransitioningDelegate together with an implementation of UIViewControllerAnimatedTransitioning. It all seems to work fine, until I do exactly the following:
open the app
present another view controller with my custom transition
rotate to landscape
dismiss the just presented view controller
That's all! What happens now is the following: I see a large black bar on the right side of the initial view controller (as if that controller's view wasn't rotated to landscape).
The funny thing is this only goes wrong in iOS 9, in iOS 8 everything seems to work just fine. Did anything change with custom transition API I don't know of? Or is this simply a really nasty iOS 9 bug? If anyone can tell me what I did wrong or if anyone can provide me with a workaround I would really appreciate that!
These classes reproduce the problem:
import UIKit
class ViewController: UIViewController, UIViewControllerTransitioningDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: "tap")
view.addGestureRecognizer(tapGestureRecognizer)
}
func tap() {
let controller = ModalViewController()
controller.transitioningDelegate = self
presentViewController(controller, animated: true, completion: nil)
}
func animationControllerForPresentedController(presented: UIViewController,
presentingController presenting: UIViewController,
sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return Transitioning()
}
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return Transitioning()
}
}
The presented view controller:
import UIKit
class ModalViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.redColor()
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: "tap")
view.addGestureRecognizer(tapGestureRecognizer)
}
func tap() {
dismissViewControllerAnimated(true, completion: nil)
}
}
And finally the UIViewControllerAnimatedTransitioning implementation:
import UIKit
class Transitioning: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.5
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)
let containerView = transitionContext.containerView()
if let fromView = fromView, toView = toView {
containerView?.addSubview(fromView)
containerView?.addSubview(toView)
toView.alpha = 0
UIView.animateWithDuration(0.5, animations: {
toView.alpha = 1
}, completion: {
finished in
transitionContext.completeTransition(true)
})
}
}
}
I generally use the following in animateTransition:
toView.frame = fromView.frame
FYI, you don't have to add fromView to the hierarchy, as it's already there.