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 have implemented a bottom sheet using a tutorial that someone else provided here. Everything is going well, however I want to implement a bottom sheet that goes above that bottom sheet and perhaps multiple other bottom sheets that go above the previous bottom sheet. I would like to implement this similar to how iOS 10 maps has their bottom sheets. i.e. if one bottom sheet goes on top of another you can simply dismiss it with pressing the 'x' at the top left.
Here is my current code:
In my map view controller I've added:
func addBottomSheetView() {
// 1- Init bottomSheetVC
let bottomSheetVC = BottomSheetViewController()
// 2- Add bottomSheetVC as a child view
self.addChildViewController(bottomSheetVC)
self.view.addSubview(bottomSheetVC.view)
bottomSheetVC.didMoveToParentViewController(self)
// 3- Adjust bottomSheet frame and initial position.
let height = view.frame.height
let width = view.frame.width
bottomSheetVC.view.frame = CGRectMake(0, self.view.frame.maxY, width, height)
}
And call it in viewDidAppear method:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
addBottomSheetView()
}
In my bottom sheet controller I've added:
override func viewDidLoad() {
super.viewDidLoad()
let gesture = UIPanGestureRecognizer.init(target: self, action: #selector(BottomSheetViewController.panGesture))
view.addGestureRecognizer(gesture)
}
func panGesture(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translationInView(self.view)
let y = self.view.frame.minY
self.view.frame = CGRectMake(0, y + translation.y, view.frame.width, view.frame.height)
recognizer.setTranslation(CGPointZero, inView: self.view)
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
UIView.animateWithDuration(0.3) { [weak self] in
let frame = self?.view.frame
let yComponent = UIScreen.mainScreen().bounds.height - 200
self?.view.frame = CGRectMake(0, yComponent, frame!.width, frame!.height)
}
}
Originally I was thinking of addSubview in the new bottom sheet, however that didn't work. Should I addSubview to the original map view controller? the button that adds the new bottom sheet is in the existing bottom sheet. Would love you advice. Thanks!
I have implemented a bottom sheet using a tutorial that someone else provided here. Everything is going well, however I want to implement a bottom sheet that goes above that bottom sheet and perhaps multiple other bottom sheets that go above the previous bottom sheet. I would like to implement this similar to how iOS 10 maps has their bottom sheets. i.e. if one bottom sheet goes on top of another you can simply dismiss it with pressing the 'x' at the top left.
Here is my current code:
In my map view controller I've added:
func addBottomSheetView() {
// 1- Init bottomSheetVC
let bottomSheetVC = BottomSheetViewController()
// 2- Add bottomSheetVC as a child view
self.addChildViewController(bottomSheetVC)
self.view.addSubview(bottomSheetVC.view)
bottomSheetVC.didMoveToParentViewController(self)
// 3- Adjust bottomSheet frame and initial position.
let height = view.frame.height
let width = view.frame.width
bottomSheetVC.view.frame = CGRectMake(0, self.view.frame.maxY, width, height)
}
And call it in viewDidAppear method:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
addBottomSheetView()
}
In my bottom sheet controller I've added:
override func viewDidLoad() {
super.viewDidLoad()
let gesture = UIPanGestureRecognizer.init(target: self, action: #selector(BottomSheetViewController.panGesture))
view.addGestureRecognizer(gesture)
}
func panGesture(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translationInView(self.view)
let y = self.view.frame.minY
self.view.frame = CGRectMake(0, y + translation.y, view.frame.width, view.frame.height)
recognizer.setTranslation(CGPointZero, inView: self.view)
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
UIView.animateWithDuration(0.3) { [weak self] in
let frame = self?.view.frame
let yComponent = UIScreen.mainScreen().bounds.height - 200
self?.view.frame = CGRectMake(0, yComponent, frame!.width, frame!.height)
}
}
Originally I was thinking of addSubview in the new bottom sheet, however that didn't work. Should I addSubview to the original map view controller? the button that adds the new bottom sheet is in the existing bottom sheet. Would love you advice. Thanks!
Is there a way to hide tabbar and remove that space left (around 50px) ?
I tried
self.tabBarController?.tabBar.hidden = true
self.extendedLayoutIncludesOpaqueBars = true
No luck. I see blank space.
If you're still seeing a black stripe under your hidden tab bar, have you tried to select Extend Edges Under Opaque Bars here?
Make also sure that Under Bottom Bars is still selected. Hope it helps!
Swift 3:
extension UITabBarController {
func setTabBarVisible(visible:Bool, duration: TimeInterval, animated:Bool) {
if (tabBarIsVisible() == visible) { return }
let frame = self.tabBar.frame
let height = frame.size.height
let offsetY = (visible ? -height : height)
// animation
UIViewPropertyAnimator(duration: duration, curve: .linear) {
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()
}.startAnimation()
}
func tabBarIsVisible() ->Bool {
return self.tabBar.frame.origin.y < UIScreen.main.bounds.height
}
}
To use (if for example self is a UITabBarController):
self.setTabBarVisible(visible: false, duration: 0.3, animated: true)
Swift 2.x:
extension UITabBarController {
func setTabBarVisible(visible:Bool, duration: NSTimeInterval, animated:Bool) {
if (tabBarIsVisible() == visible) { return }
let frame = self.tabBar.frame
let height = frame.size.height
let offsetY = (visible ? -height : height)
// animation
UIView.animateWithDuration(animated ? duration : 0.0) {
self.tabBar.frame = CGRectOffset(frame, 0, offsetY)
self.view.frame = CGRectMake(0, 0, self.view.frame.width, self.view.frame.height + offsetY)
self.view.setNeedsDisplay()
self.view.layoutIfNeeded()
}
}
func tabBarIsVisible() ->Bool {
return self.tabBar.frame.origin.y < UIScreen.mainScreen().bounds.height
}
}
To use:
self.tabBarController?.setTabBarVisible(visible: false, duration: 0.3, animated: true)
After saw your screenshot in comment. I think you can try to set hidesBottomBarWhenPushed to true.
hidesBottomBarWhenPushed = true
Or storyboard.
It will hide bottom bar automatically when you pushed to another view controller, and appear it again when you go back.
Programmatically, add this to the next view controller for swift 4.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tabBarController?.tabBar.isHidden = true
edgesForExtendedLayout = UIRectEdge.bottom
extendedLayoutIncludesOpaqueBars = true
}
And add a background color
NOTE - This solution is to just to remove white space left after hiding tab bar.
For hiding tab bar best solution is - #Michael Campsall answer here
The simplest solution to this is to change your view's(in my case its tableView) bottom constraints, instead of giving bottom constraints with BottomLayoutGuide give it with superview. Screenshots attached for reference.
Constraints shown in below screenshots creates the problem, change it according to next screenshot.
Actual constraints to remove white space should be according to this(below) screenshot.
For those that like to do everything programmatically, add this line to the init method of a ViewController that shouldn't have the tabBar:
hidesBottomBarWhenPushed = true
I was facing the same issue and root cause was BOTTOM CONSTRAINT
Make sure you set the bottom constraint of your bottom most view in the main view hierarchy with SUPERVIEW, NOT "SAFE AREA"
Hope this helps someone..
The third answer on this question works for me in the following way:
The code on my view controller
#IBAction func buttonPressed(sender: AnyObject) {
setTabBarVisible(!tabBarIsVisible(), animated: true)
}
func setTabBarVisible(visible: Bool, animated: Bool) {
// hide tab bar
let frame = self.tabBarController?.tabBar.frame
let height = frame?.size.height
var offsetY = (visible ? -height! : height)
print ("offsetY = \(offsetY)")
// zero duration means no animation
let duration:NSTimeInterval = (animated ? 0.3 : 0.0)
// animate tabBar
if frame != nil {
UIView.animateWithDuration(duration) {
self.tabBarController?.tabBar.frame = CGRectOffset(frame!, 0, offsetY!)
self.view.frame = CGRectMake(0, 0, self.view.frame.width, self.view.frame.height + offsetY!)
self.view.setNeedsDisplay()
self.view.layoutIfNeeded()
return
}
}
}
func tabBarIsVisible() -> Bool {
return self.tabBarController?.tabBar.frame.origin.y < UIScreen.mainScreen().bounds.height
}
In storyboard:
The view controller main view background color is black color:
Then you could have another view inside (background color white), constrained trailing and leading space to superview and top and bottom space to the layout guide.
And the result is:
My preferred way to do that is using a wrapping controller. If I want to hide the tab bar, I just increase the height of the tab bar controller, thus effectively the tab bar is moved out of the screen.
With this solution you don't need to hack tab bar frame and you don't depend on navigation controller push animation:
import UIKit
class ViewController: UIViewController {
let tabController: UITabBarController = {
let tabController = UITabBarController()
// setup your tabbar controller here
return tabController;
}()
var tabbarHidden = false {
didSet {
var frame = self.view.bounds;
if (tabbarHidden) {
frame.size.height += self.tabController.tabBar.bounds.size.height;
}
self.tabController.view.frame = frame;
}
}
override func viewDidLoad() {
super.viewDidLoad()
// add the tab controller as child controller
addChildViewController(self.tabController)
self.tabController.view.frame = self.view.bounds
self.tabController.view.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
self.view.addSubview(self.tabController.view)
self.tabController.didMoveToParentViewController(self)
// for debugging
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(switchTabbar))
self.tabController.view.addGestureRecognizer(tapRecognizer)
}
override func childViewControllerForStatusBarStyle() -> UIViewController? {
return self.tabController
}
override func childViewControllerForStatusBarHidden() -> UIViewController? {
return self.tabController
}
func switchTabbar() {
UIView.animateWithDuration(0.3) {
self.tabbarHidden = !self.tabbarHidden
}
}
}
try to set the tab bar translucent to before you hide the tab bar set to false again when you want to show again.
it works for me.
tabBarController?.tabBar.isTranslucent = true
Yes. You can hide your tab bar when you push to view controller. You can show tab bar in your home. You can hide your tab bar when you push to next View controller.
See the Hide Botton Bar on Push following image and set in all viewcontrollers where you dont want tab bar.
Hope it helps..
Sometimes that easiest way is just to add a view that uses the UIScreen bounds.
let whiteView = UIView()
whiteView.backgroundColor = .white
view.addSubview(whiteView)
whiteView.translatesAutoresizingMaskIntoConstraints = false
whiteView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
whiteView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
whiteView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
whiteView.heightAnchor.constraint(equalToConstant: UIScreen.main.bounds.height).isActive = true
Cause sometimes the view edges extends beyond the nav bar giving you new problems if you extend the view layout.
Tested in Swift 5.4.
If you're adding any ViewController's view as subview programmatically and not using pushViewController, then you can simply try as follows:
// When you wanna hide TabBar
tabBarController?.tabBar.isHidden = true
tabBarController?.tabBar.isTranslucent = true // This is the key point!
// When you wanna show TabBar
tabBarController?.tabBar.isHidden = false
tabBarController?.tabBar.isTranslucent = false // This is the key point!
This code works on iOS 10, 11, and iPhone X (including simulators) to show/hide the tabBar. I created it several years (iOS 7 time frame?) and it has worked reliably since that time.
It works great on iPhone X as long as content content in your childViewControllers (in tabs) is pinned to topLayoutGuide, bottomLayoutGuide or SafeArea and not the main views walls. Then it all just works. Enjoy!
#interface UITabBarController (HideTabBar)
#property (nonatomic, getter=isTabBarHidden) BOOL tabBarHidden;
-(void)setTabBarHidden:(BOOL)hidden animated:(BOOL)animated;
#end
#implementation UITabBarController (HideTabBar)
-(BOOL)isTabBarHidden
{
CGRect viewFrame = self.view.frame;
CGRect tabBarFrame = self.tabBar.frame;
return tabBarFrame.origin.y >= viewFrame.size.height;
}
-(void)setTabBarHidden:(BOOL)hidden
{
[self setTabBarHidden:hidden animated:NO];
}
-(void)setTabBarHidden:(BOOL)hidden animated:(BOOL)animated
{
BOOL isHidden = self.tabBarHidden;
if(hidden == isHidden)return;
UIView *transitionView = [[[self.view.subviews reverseObjectEnumerator] allObjects] lastObject];
if(transitionView == nil) {
NSLog(#"UITabBarCategory can't get the container view");
return;
}
CGRect viewFrame = self.view.bounds;
CGRect tabBarFrame = self.tabBar.frame;
CGRect containerFrame = transitionView.frame;
CGRect selectedVCFrame = containerFrame;
tabBarFrame.origin.y = viewFrame.size.height - (hidden ? 0 : tabBarFrame.size.height);
containerFrame.size.height = viewFrame.size.height - (hidden ? 0 : tabBarFrame.size.height);
if([self.moreNavigationController.viewControllers containsObject:self.selectedViewController]) {
selectedVCFrame = self.selectedViewController.view.frame;
selectedVCFrame.size.height += hidden ? tabBarFrame.size.height : -tabBarFrame.size.height;
}
self.selectedViewController.view.frame = selectedVCFrame;
[UIView animateWithDuration:.5 animations:^{
self.tabBar.frame = tabBarFrame;
transitionView.frame = containerFrame;
[self.selectedViewController.view setNeedsLayout];
}];
}
#end
Usage - I call it in the viewController on rotation events like so:
-(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
[super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
// Hide TabBar on iPhone, iPod Touch
if([UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPad) {
if(_startDateEditor.editing) return;
if(fromInterfaceOrientation == UIInterfaceOrientationPortraitUpsideDown || fromInterfaceOrientation == UIInterfaceOrientationPortrait)
[self.tabBarController setTabBarHidden:YES animated:YES];
else
[self.tabBarController setTabBarHidden:NO animated:YES];
}
}
For me in iOS 13 I had to display image in cell with full screen, I had collection view with trailing, leading, top, bottom constraint. I removed all constraint. set collection view frame to UIScreen.main.bounds. then return sizeForItemAt as collection frame size.
I have a custom keyboard for a UITextField using the inputView. I can switch over to the native keyboard again, at which point the height of the keyboard can change (e.g. swiping up/down on autosuggest, changing keyboards). This is why my custom keyboard inputView needs to be able to change its height.
However, when the frame height of the inputView is change, it magically resets itself again!
(I'm doing this on iOS 8.4 simulator.)
Simplifying the problem to the most basic example:
func createInputView(#height:CGFloat) -> UIView {
let view = ViewSubclass(frame: CGRectMake(0, 0, CGRectGetWidth(self.view.bounds), height))
view.backgroundColor = UIColor.redColor()
let label = UILabel(frame: CGRectMake(0, 0, 100, 30))
label.backgroundColor = UIColor.yellowColor()
label.text = "Hello!"
view.addSubview(label)
return view
}
// halves the view's frame height
func changeFrameHeightForView(view: UIView) {
view.frame = {
var f = view.frame
f.size.height = f.size.height / 2.0
return f
}()
}
override func viewDidLoad() {
super.viewDidLoad()
view1 = createInputView(height: 200)
view2 = createInputView(height: 150)
myTextField.inputView = view1
}
// to reproduce this scenario, you would first focus on the text field to open the keyboard, and then tap on the controller's view, which triggers the below action:
// example 1
#IBAction func viewTapped(sender: AnyObject) {
let currentInputView = self.myTextField.inputView!
println("starting height: \(currentInputView.frame.height)")
// starting height: 200.0
changeFrameHeightForView(currentInputView)
println("height after frame changed: \(currentInputView.frame.height)")
// height after frame changed: 100.0
self.myTextField.reloadInputViews()
println("height after reloadInputViews: \(currentInputView.frame.height)")
// height after reloadInputViews: 100.0
// if I stopped here, the view will update and be shorter, but it shows a grey area underneath - the keyboard itself hasn't changed height! So try to dismiss the keyboard and reload it again...
myTextField.resignFirstResponder()
println("height after resign first responder: \(currentInputView.frame.height)")
// height after resign first responder: 200.0 // --> HOW DID IT GET REVERTED???
myTextField.becomeFirstResponder()
println("height after become first responder: \(currentInputView.frame.height)")
// height after become first responder: 200.0
}
So the question for example 1 is:
Why did the frames get reverted as soon as resignFirstResponder gets called?
I start thinking, maybe the text field is doing something to the inputView. So what if I nil it out. Then it shouldn't be able to affect the original view right? This is where it gets weird...
// example 2
#IBAction func viewTapped(sender: AnyObject) {
let currentInputView = self.myTextField.inputView!
println("starting height: \(currentInputView.frame.height)")
// starting height: 200.0
// remove the inputView and close the keyboard.
self.myTextField.inputView = nil
self.myTextField.reloadInputViews()
myTextField.resignFirstResponder()
// change the frame height
changeFrameHeightForView(currentInputView)
println("height after frame changed: \(currentInputView.frame.height)")
// height after frame changed: 100.0
// set it as the inputView again
self.myTextField.inputView = currentInputView
self.myTextField.reloadInputViews()
println("height after reloadInputViews: \(currentInputView.frame.height)")
// height after reloadInputViews: 100.0
// open the keyboard
myTextField.becomeFirstResponder()
println("height after become first responder: \(currentInputView.frame.height)")
// height after become first responder: 200.0 // --> HOW DID IT GET REVERTED??? Shouldn't the keyboard use the new frame height?
}
Moving onto example 3...
Those with a keen eye will notice i have 2 input views set up. Here I will toggle the inputView between them. Interestingly, if a different view object gets assigned, it all works as expected:
// example 3
#IBAction func viewTapped(sender: AnyObject) {
let currentInputView = self.myTextField.inputView!
let otherView = (currentInputView == self.view1 ? self.view2 : self.view1)
self.myTextField.inputView = otherView
self.myTextField.reloadInputViews()
}
Apologies for the long question, but I've documented my findings in this investigation. I don't really want to create a brand new view every time the keyboard changes height.
Facebook messenger's custom keyboard is able to do something similar to this, so I'm hoping someone can shed some light on this.
thanks!