I subclassed UINavigationBar, and I have a button that drops down into a drop down menu. I wish to animate and push the contents below the navigation bar down as the navigation bar increases in height. Essentially I want a temporary table in the view of the navigationBar.
So far I overrode sizeThatFits() to return the custom sizes for the length I want, which is determined by a state enum. And I call sizeToFit() when the navigation bar needs to resize.
override func sizeThatFits(size: CGSize) -> CGSize {
let superSize = super.sizeThatFits(size)
var newSize: CGSize!
if(state == .Normal) {
newSize = CGSizeMake(superSize.width, superSize.height + heightIncrease)
} else if (state == .Menu) {
newSize = CGSizeMake(superSize.width, superSize.height + tableIncrease)
}
return newSize
}
func buttonPressed(){
if(state == .Normal){
state = .Menu
_delegate?.didChangeStateTo(.Menu)
} else if (state == .Menu){
state = .Normal
_delegate?.didChangeStateTo(.Normal)
}
UIView.animateWithDuration(0.3, animations: {
self.sizeToFit()
})
}
When I animate sizeToFit(), the navigation bar increases in size according to the animation, but the contents below the bar (the view of the viewController) immediately jumps down to the full length.
This is because sizeThatFits immediately returns the new CGSize. So the animation only works for the navigation bar itself, and the contents of the view do not follow the animation, but jump to their destination.
I tried avoiding using sizeThatFits, but UINavigationBar doesn't allow you to change the size of the bar simply by setting the frame with a CGRect.
I know there are other ways of going about this without playing around with subclassing UINavigationBar, but with my app's UI this way would be the most effective and code efficient.
Thanks in advance
Related
I have enabled large titles for the navigation bar with:
navigationController?.navigationBar.prefersLargeTitles = true
This makes the navigation bar start with an expanded height, and shrink as the user scrolls down.
Now, I want to add a subview inside the navigation bar that resizes, based on how tall the navigation bar is. To do this, I will need to get both the maximum and minimum height of the navigation bar, so I can calculate the fraction of how much it's expanded.
I can get the current height of the navigation bar like this:
guard let height = navigationController?.navigationBar.frame.height else { return }
print("Navigation height: \(height)")
I'm calling this inside scrollViewDidScroll, and as I'm scrolling, it seems that the expanded height is around 96 and the shrunk height is around 44. However, I don't want to hardcode values.
iPhone 12
Expanded (96.33)
Shrunk (44)
iPhone 8
Expanded (96.5)
Shrunk (44)
I am also only able to get these values when the user physically scrolls up and down, which won't work in production. And even if I forced the user to scroll, it's still too late, because I need to know both heights in advance so I can insert my resizing subview.
I want to get these values, but without hardcoding or scrolling
Is there any way I can get the height of both the shrunk and expanded navigation bar?
Came across my own question a year later. The other answer didn't work, so I used the view hierarchy.
It seems that the shrunk appearance is embedded in a class called _UINavigationBarContentView. Since this is a private class, I can't directly access it. But, its y origin is 0 and it has a UILabel inside it. That's all I need to know!
extension UINavigationBar {
func getCompactHeight() -> CGFloat {
/// Loop through the navigation bar's subviews.
for subview in subviews {
/// Check if the subview is pinned to the top (compact bar) and contains a title label
if subview.frame.origin.y == 0 && subview.subviews.contains(where: { $0 is UILabel }) {
return subview.bounds.height
}
}
return 0
}
}
Usage:
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Navigation"
if
let navigationBar = navigationController?.navigationBar,
let window = UIApplication.shared.keyWindow
{
navigationBar.prefersLargeTitles = true /// Enable large titles.
let compactHeight = navigationBar.getCompactHeight() // 44 on iPhone 11
let statusBarHeight = window.safeAreaInsets.top // 44 on iPhone 11
let navigationBarHeight = compactHeight + statusBarHeight
print(navigationBarHeight) // Result: 88.0
}
}
The drawback of this answer is if Apple changes UINavigationBar's internals, it might not work. Good enough for me though.
Using following extension u can get extra height
extension UINavigationBar
{
var largeTitleHeight: CGFloat {
let maxSize = self.subviews
.filter { $0.frame.origin.y > 0 }
.max { $0.frame.origin.y < $1.frame.origin.y }
.map { $0.frame.size }
return maxSize?.height ?? 0
}
}
And I said earlier u can get extended height by following
guard let height = navigationController?.navigationBar.frame.maxY else { return }
print("Navigation height: \(height)")
let window = UIApplication.shared.keyWindow
let topPadding = window?.safeAreaInsets.top
let extendedHeight = height - topPadding
You can get shrunk height by subtracting difference from extended height
guard let difference = navigationController?.navigationBar.lagreTitleHeight else {return}
let shrunkHeight = extendedHeight - difference
I have a UIViewController that has tab bar controller at bottom. When user click on a button I m hiding the tab bar. Tab bar is getting hidden but there is a white space at bottom. ViewController frame is not changing. How to manage this ? If tabor controller gets hidden, viewController height should get increase.
func apply(_ effect: ActivityFeedEffect) {
switch effect {
case .feedTypeChange(mode: let mode):
self.parent?.tabBarController?.tabBar.isHidden = mode == .hidden
}
}
This is an extension on UITabBarController, which you can use.
This basically, updates the frames of the view.
You can add animation and other frame handling if needed, based on your use case. But this is something that can lead you in that direction.
extension UITabBarController {
func hideTabBar(isHidden:Bool) {
if (isTabBarAlreadyHidden() == isHidden) { return }
let frame = self.tabBar.frame
let height = frame.size.height
let offsetY = (isHidden ? -height : height)
self.tabBar.frame.offsetBy(dx:0, dy:offsetY)
self.view.frame = CGRect(x:0,y:0,width: self.view.frame.width, height: self.view.frame.height + offsetY)
self.view.setNeedsDisplay()
self.view.layoutIfNeeded()
}
func isTabBarAlreadyHidden() ->Bool {
return self.tabBar.frame.origin.y < UIScreen.main.bounds.height
}
}
In my case, I have configured on the storyboard the extended Edges to go under bottom bars and under opaque bars (see image). So My view always takes the hole screen, and I don't need to adjust the frame. Maybe this helps.
My structure is Tab bar -> Navigation Controller -> TableView (here I hide/show the tab bar)
I have a "classic app" with 3 ViewController and a tabBar that I use to change ViewController.
On my first ViewController, I have a button that display a UIView on all the screen, so I hide tabBar with this setTabBarVisible func :
extension UIViewController
{
func setTabBarVisible(visible: Bool, animated: Bool)
{
//* This cannot be called before viewDidLayoutSubviews(), because the frame is not set before this time
// bail if the current state matches the desired state
if (isTabBarVisible == visible) { return }
// get a frame calculation ready
let frame = self.tabBarController?.tabBar.frame
let height = frame?.size.height
let offsetY = (visible ? -height! : height)
// zero duration means no animation
let duration: TimeInterval = (animated ? 0.3 : 0.0)
// animate the tabBar
if frame != nil
{
UIView.animate(withDuration: duration)
{
self.tabBarController?.tabBar.frame = frame!.offsetBy(dx: 0, dy: offsetY!)
return
}
}
}
var isTabBarVisible: Bool
{
return (self.tabBarController?.tabBar.frame.origin.y ?? 0) < self.view.frame.maxY
}
}
That's working, the tabBar is hidden and I see all my UIVIew.
The problem is, I have a UILabel at bottom of the UIView (at the place I usually display the tabBar), and I can't use my TapGesture on my UILabel, nothing is happening.
(if I display the label somewhere else the UITapGesture works good.)
I tried to set zPosition of my tabBar to 0 and zPosition of my UIView to 1 but that's doesn't work either.
How can I get my label clickable at bottom of my view?
Check UILabel.isUserInterration = enable
Make sure that when you hide tabbar, perticular view controller Under bottom bar property is unselect . See atteh imnage.
You can try with programatically also like
ViewController.edgesForExtendedLayout = UIRectEdge.top
make sure is true
label.isUserInteractionEnabled = true
Please refer this link may it help you.
I am facing this situation where the navigation bar looks OK in portrait mode but gets cropped in landscape:
portrait
landscape
I subclassed UINavigationBar as follows:
class CustomNavigationBar: UINavigationBar {
override func sizeThatFits(size: CGSize) -> CGSize {
let newSize :CGSize = CGSize(width: self.frame.size.width, height: 64)
return newSize
}
}
and assigned it to the appropriate Navigation Controller via the StoryBoard:
but it makes no difference.
Any ideas?
Navigation bar has different height in Portrait and Landscape mode. You should handle content of custom title view according to navigation bar height. Use autolayout to auto adjust the subview when navigation bar's height changes.
I solved it by overriding sizeThatFits func in an extension. The idea is that it resets the size back to 44 which is the default for portrait:
// prevent navigation bars from resizing in landscape
extension UINavigationBar {
public override func sizeThatFits(size: CGSize) -> CGSize {
let portraitSize = CGSizeMake(UIScreen.mainScreen().bounds.width, 44)
return portraitSize
}
}
Update
I found that in my many refactorings I was inheriting from UIViewController instead of UITableViewController, so I was missing some automatic behaviours that UITableViewController provides. However, I still needed to manually handle the scroll views insets when the keyboard was being interactively dismissed. See my updated answer.
I am trying to emulate iMessage in how the keyboard is dismissed when the user drags it to the bottom of the screen. I have it working with one small visual issue that's bugging me.
As the keyboard is dragged off the screen the scroll indicators do not resize correctly - that is until it has been completely dismissed.
I use keyboard notifications to tell me when the keyboard has appeared to increase the content and scroll insets by the height of the keyboard. It seems I didn't need to do anything when the keyboard has been dismissed as the insets appear to be correct when it has been. However when dismissing interactively I can't update the insets during the dragging event.
To illustrate the issue, the first image shows that content has scrolled off the top of the screen due to the space being occupied by the keyboard; the user has scrolled to the last row in the table:
Here, the keyboard is being dismissed and is almost completely off-screen. However notice how the scroll indicators are completely the wrong size. All of the content is now almost on screen so the indicators should be stretching, however, what happens is that as the keyboard moves down, the scroll indicators move up and do not stretch. This is not what happens in iMessage.
I think what I'm doing is pretty standard, I'm creating a UIToolBar (iOS 8.3) and overriding these methods in my view controller:
override var inputAccessoryView: UIView {
return toolbar
}
override func canBecomeFirstResponder() -> Bool {
return true
}
func willShowKeyboard(notification: NSNotification) {
let keyboardFrame = notification.userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue
tableView.contentInset.bottom = keyboardFrame.CGRectValue().height
tableView.scrollIndicatorInsets.bottom = keyboardFrame.CGRectValue().height
}
Update
After switching to a UITableViewController, I found that this implementation of scrollViewDidScroll() (along with the other methods in the original solution below) did the trick of dynamically resizing the insets when the keyboard was interactively dismissed.
override func scrollViewDidScroll(scrollView: UIScrollView) {
if !keyboardShowing {
return
}
let toolbarFrame = toolbar.convertRect(toolbar.frame, toView: nil)
tableView.scrollIndicatorInsets.bottom = view.bounds.height - toolbarFrame.minY
tableView.contentInset.bottom = view.bounds.height - toolbarFrame.minY
}
I've managed to achieve the same effect. I'm not sure if this is the correct method, but it works nicely. I'll be interested to know what other solutions there might be.
func didShowKeyboard(notification: NSNotification) {
let keyboardFrame = notification.userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue
let keyboardHeight = keyboardFrame.CGRectValue().height
tableView.contentInset.bottom = keyboardHeight
tableView.scrollIndicatorInsets.bottom = keyboardHeight
keyboardShowing = true
}
func didHideKeyboard(notification: NSNotification) {
keyboardShowing = false
}
func scrollViewDidScroll(scrollView: UIScrollView) {
if !keyboardShowing {
return
}
let toolbarFrame = view.convertRect(toolbar.frame, fromView: toolbar)
tableView.scrollIndicatorInsets.bottom = view.bounds.height - toolbarFrame.minY
tableView.contentInset.bottom = view.bounds.height - toolbarFrame.minY
}