iOS Dark Keyboard has background during UINavigationController push/pop animation - ios

During the navigation controller push/pop animation, keyboard is darker then it is in its final state. And on animation end, this black background view just disappears. Light (white) keyboard style does not have this effect.
How can I get rid of this black background?
I already tried setting the window color to white and setting the navigation controller background to white.
VIDEO:
https://www.dropbox.com/s/z1grj821fj306th/Untitled.mov?dl=0
SCREENSHOT:

Option 1
The easiest solution is to just to disable the keyboard transparency by setting its background color to black or white (depending on if you have a light or dark keyboard):
myTextField.becomeFirstResponder()
guard UIDevice().userInterfaceIdiom == .phone else {
return;
}
//keyboard window should be there now, look for it:
var keyboardWindow: UIWindow?
for window in UIApplication.shared.windows.reversed() {
if String(describing: type(of: window)) == "UIRemoteKeyboardWindow" {
keyboardWindow = window
break
}
}
keyboardWindow?.rootViewController?.view.subviews.first?.backgroundColor = .black
Option 2
If you don't like this I have a hack that seems to work pretty well, at least in iOS 14. It results in the keyboard slide up animation occurring during the slide in push animation, instead of after. It relies on showing the keyboard right before pushing by adding a temporary text field.
Run this code whenever you want to push the VC:
guard UIDevice().userInterfaceIdiom == .phone else {
// fix doesn't apply to iPad, push or perform segue:
self.performSegue(withIdentifier: "mySegue", sender: self)
// navigationController?.pushViewController(destinationVC, animated: true)
return;
}
//create text field with matching settings so keyboard will look the same as its destination
let tempTextField = UITextField.init()
tempTextField.keyboardType = .default
tempTextField.keyboardAppearance = .light
tempTextField.autocorrectionType = .default
view.addSubview(tempTextField)
//show keyboard, then right after push VC, then discard the text field
//make sure destination text field calls becomeFirstResponder() in its VC's viewDidLoad()
tempTextField.becomeFirstResponder()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0/60.0) {
// push or perform segue:
self.performSegue(withIdentifier: "mySegue", sender: self)
// navigationController?.pushViewController(destinationVC, animated: true)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
tempTextField.removeFromSuperview()
}
}
Option 3
I improved on option 2 by copying the push slide animation to the keyboard, essentially resulting in exactly what you asked. There is also the option to keep the sliding up animation too during the slide in, just give it a try.
Run this code on the pushing VC whenever you want to push:
guard UIDevice().userInterfaceIdiom == .phone else {
// fix doesn't apply to iPad, push or perform segue:
self.performSegue(withIdentifier: "mySegue", sender: self)
// navigationController?.pushViewController(destinationVC, animated: true)
return;
}
//create text field with matching settings so keyboard will look the same as its destination
let tempTextField = UITextField.init()
tempTextField.keyboardType = .default
tempTextField.keyboardAppearance = .light
tempTextField.autocorrectionType = .default
view.addSubview(tempTextField)
//show keyboard, then push VC, then remove the text field (see bottom)
//make sure destination text field calls becomeFirstResponder() in its VC's viewDidLoad()
UIView.setAnimationsEnabled(false) //set to false to disable slide up animation or true to keep it
tempTextField.becomeFirstResponder()
UIView.setAnimationsEnabled(true)
//find keyboard window
var keyboardWindow: UIWindow?
for window in UIApplication.shared.windows.reversed() {
if String(describing: type(of: window)) == "UIRemoteKeyboardWindow" {
keyboardWindow = window
break
}
}
keyboardWindow?.rootViewController?.view.isHidden = true //this prevents glitches
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0/60.0) {
// push or perform segue
self.performSegue(withIdentifier: "mySegue", sender: self)
// navigationController?.pushViewController(destinationVC, animated: true)
keyboardWindow?.rootViewController?.view.isHidden = false
//this spring animation is identical to default push slide animation
let spring = CASpringAnimation(keyPath: "position")
spring.damping = 500
spring.mass = 3
spring.initialVelocity = 0
spring.stiffness = 1000
spring.fromValue = CGPoint.init(x: self.view.frame.width, y:0) //you can enter e.g y:1000 to delay slide up animation
spring.toValue = CGPoint.init(x: 0, y:0)
spring.duration = 0.5
spring.isAdditive = true
keyboardWindow?.layer.add(spring, forKey: nil)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
tempTextField.removeFromSuperview()
}
You can keep the sliding up animation by not disabling the animations (above becomeFirstResponder()). If you choose this it is possible to delay the slide up animation by replacing y:0 with y:1000 for example. You can play with this value.

Related

UIView.animate not setting image and label alphas on VC change

EDIT: I figured out I need to resume the layer if its paused, but am not sure how to resume the layer when the view returns. viewDidLoad doesn't seem to trigger on segue
I have an app with an animation, sound, and a webView. The animation is triggered by a button and a segue to the webView is triggered by another button. If the animation is playing when the app segues to the webView, once the app segues back, I have it set so the animation restarts when the play button is tapped. This works fine
However, when the animation is paused and the view changes, upon returning to the main view, the image alpha and label alpha doesn't appear to animate... although the debugger says the alpha is changing appropriately, nothing appears on screen.
How can I get it to animate appropriately when play is tapped after returning from the WebView?
Here's the play button:
touch += 1
//define animated layers
imgLayer = firstImg.layer
labelLayer = firstLbl.layer
if touch == 1 {
print("touch = \(touch)")
self.firstImg.alpha = 0
self.firstLbl.text = ""
animateStart()
player.play()
self.playOut.setBackgroundImage(UIImage(named: "pause.png"), for: UIControlState.normal)
} else {
touch = 2
}
//toggle pause
if touch == 2 {
print("touch = \(touch)")
pause = !pause
if pause {
animatePause()
player.pause()
} else {
animateResume()
player.play()
}
}
}
and the animate start block:
func animateStart() {
print("animation started")
UIView.animate(withDuration: 4, animations: {
self.firstImg.alpha = 1
print("firstImg alpha = \(self.firstImg.alpha)")
self.firstImg.image = UIImage(named:"peter.JPG")
self.view.layoutIfNeeded()
}, completion: { finished in
if finished {
self.animateSecond()
}
})
}
and here's how I dismiss the VC on segue
func dismissVC() {
touch = 0
self.playOut.setBackgroundImage(UIImage(named: "playbtn.png"), for: UIControlState.normal)
player.stop()
initAudio()
self.dismiss(animated: true, completion: nil)
}

Set picker values by tapping anywhere outside of a popup view in iOS

Currently have a popup view which sets 3 picker values by tapping on the SET button:
However, I want to remove the SET button altogether, and have the picker values set upon tapping outside of the popup, which in turn hides the popup.
Here is the current code:
// function for selecting picker values
func pickerDidSet() {
let focusPeriodChoice = focusPeriodDataSource[pickerView.selectedRow(inComponent: 0)]
let breakPeriodChoice = breakPeriodDataSource[pickerView.selectedRow(inComponent: 1)]
let repeatCountChoice = repeatCountDataSource[pickerView.selectedRow(inComponent: 2)]
persistPickerChoice(focusPeriodChoice, dataType: .focusPeriod)
persistPickerChoice(breakPeriodChoice, dataType: .breakPeriod)
persistPickerChoice(repeatCountChoice, dataType: .repeatCount)
timerSummaryLabel.text = "\(focusPeriodChoice)m • \(breakPeriodChoice)m • \(repeatCountChoice)x"
UIView.animate(withDuration: 0.2, animations: { self.pickerContainerView.alpha = 0.0 }, completion: { finished in
self.pickerContainerView.isHidden = true
})
}
// Open popup, by tapping gear icon
#IBAction func openSettings(_ sender: Any) {
pickerView.selectRow(pickerChoiceIndex(forDataType: .focusPeriod), inComponent: 0, animated: false)
pickerView.selectRow(pickerChoiceIndex(forDataType: .breakPeriod), inComponent: 1, animated: false)
pickerView.selectRow(pickerChoiceIndex(forDataType: .repeatCount), inComponent: 2, animated: false)
self.pickerContainerView.isHidden = false
UIView.animate(withDuration: 0.2) {
self.pickerContainerView.alpha = 1.0
}
}
// Once pickers have been set, display the summary
private func configureSummaryLabel() {
let focusPeriodChoice = pickerChoice(forDataType: .focusPeriod)
let breakPeriodChoice = pickerChoice(forDataType: .breakPeriod)
let repeatCountChoice = pickerChoice(forDataType: .repeatCount)
timerSummaryLabel.text = "\(focusPeriodChoice)m • \(breakPeriodChoice)m • \(repeatCountChoice)x"
}
// Setting the picker “SET” button
private func addPickerSetButton(atX x: CGFloat, centerY: CGFloat) {
pickerSetButton.frame = CGRect(x: x, y: 0, width: 40, height: 20)
pickerSetButton.center = CGPoint(x: pickerSetButton.center.x, y: centerY)
pickerSetButton.setTitle("SET", for: .normal)
pickerSetButton.setTitleColor(UIColor.white, for: .normal)
pickerSetButton.setTitleColor(UIColor.darkGray, for: .highlighted)
pickerSetButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 17)
pickerSetButton.addTarget(self, action: #selector(pickerDidSet), for: .touchUpInside)
pickerHeaderView.addSubview(pickerSetButton)
}
If the Previous Black View is you default view of ViewController then all you need is to implemented below method.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Check that the touched view is your background view
if touches.first?.view == self.view {
// Do What Every You want to do
}
}
Detail
Every ViewController has a default view object. As in your case the black overlay displaying behind your popup seems like the default view of that view controller. If that black overlay is not your default view then create and IBOutlet of that view which is black opacified in color. And then in the above method where you are check that which view is touch check that if touched view is your black view or not.
Suppose you black view's IBOutlet is backgroundView then the above check will be something like this.
if touches.first?.view == self.backgroundView {
//It means you have touched outside the pop and out side the pop there is only your backgroundView.
//Here you should do exactly the same which you were doing when `SET` button was clicked.
}
touchesBegan method didn't work if touched object is a button so as per you logic.
You need to check if the PickerView is visible then disable it instead of firing the other feature of that button.
Example.
Create a boolean variable named isPickerViewVisible in your class and when picker view is going to visible make it true and when picker view is getting hide just make it false. There might be an IBAction for that red button.
#IBAction didTapButton(_ sender: Any){
//Here you need to check if pickerView is open then disable it. I don't know what logic you have implemented to show picker view.
if isPickerViewVisible {
self.pickerDidSet()
}else {
//Here you should do the task that you do on clicking this button.
}
}

Simple way to set Button Active Color in iOS Swift

Ive been looking around for methods to change active state on Button click. I have 4 buttons when clicked the reposition my scroll view as programmed.
I am trying to set the background color to fade a bit if clicked. Im able to set the background color but it stays the same faded color when another is clicked. It doesnt return to an inactive state.
Any simple way to acheive this onckick button behavior globally?
heres my button click func's:
#IBAction func tab1(sender: UIButton)
{
slScrollView.setContentOffset(CGPointMake(0.0, 0.0), animated: true)
tab1.backgroundColor = UIColor.grayColor()
tab2.selected = false
tab3.selected = false
}
#IBAction func tab2(sender: UIButton)
{
slScrollView.setContentOffset(CGPointMake(0.0, 650.0), animated: true)
tab2.backgroundColor = UIColor.grayColor()
tab1.selected = false
tab3.selected = false
}
#IBAction func tab3(sender: UIButton) {
slScrollView.setContentOffset(CGPointMake(0.0, 1370.0), animated: true)
tab3.backgroundColor = UIColor.grayColor()
tab1.selected = false
tab2.selected = false
}
First, create an IBOutletCollection (or four separate outlets) to the buttons. Then create an IBAction method and set all four buttons to fire it when tapped. In the method, do the background fade animation on the button that fired the action (which is passed into the handler as its sender argument, then reset the states of the other outlet buttons.
The way I would code it:
// Outlet to all of the buttons. ctrl+drag each button to this outlet.
#IBOutletCollection buttons = [UIButton]()
// Set *all* of the buttons to fire this method.
#IBAction func buttonTapped(sender: AnyObject!) {
(sender as? UIButton).backgroundColor = <whatever>
for button in buttons.filter({ $0 != sender }) {
button.backgroundColor = <default>
}
}

How to choose the right UIGestureRecognizer?

I have view controller which as UIPageViewController inside.
Using pageviewcontroller I can swipt left, right in order to go to other VCs. It works!
So, after I added sideBarMenu. When adding this menu I use this code to add gesture recognizer:
var menuViewController: UIViewController! {
didSet {
self.exitPanGesture = UIPanGestureRecognizer()
self.exitPanGesture.addTarget(self, action:"handleOffstagePan:")
self.sourceViewController.view.addGestureRecognizer(self.exitPanGesture)
}
Here the sourceViewController is my original VC.
The problem is when I try to swipe (in order to close menu), the pageViewController swipe works.
I want to disable pageViewController swipe and enable new swipe function when menu is opened. And do oppositely when menu is closed.
Additional code:
func handleOffstagePan(pan: UIPanGestureRecognizer){
println("dismiss pan gesture recognizer")
let translation = pan.translationInView(pan.view!)
let d = translation.x / CGRectGetWidth(pan.view!.bounds) * -0.5
switch (pan.state) {
case UIGestureRecognizerState.Began:
self.interactive = true
self.menuViewController.performSegueWithIdentifier("dismisMenu", sender: self)
break
case UIGestureRecognizerState.Changed:
self.updateInteractiveTransition(d)
break
default:
self.interactive = false
if d > 0.1 {
self.finishInteractiveTransition()
}else {
isMenuVisible = false
self.cancelInteractiveTransition()
}
}
}
Guys!
SO, the solution is instead of setting PageViewController to the sourceVC of your TransitionManager, set pageContentViewController to the sourceVC. PageContentViewControler is :
func resetToMainPage(index: Int!) {
/* Getting the page View controller */
mainPageViewController = self.storyboard?.instantiateViewControllerWithIdentifier("MainPageViewController") as UIPageViewController
self.mainPageViewController.dataSource = self
self.mainPageViewController.delegate = self
let pageContentViewController = self.viewControllerAtIndex(index)
self.transtionManger.sourceViewController = pageContentViewController // adding swipe to the pageContentViewControlle in order to close menu
self.mainPageViewController.setViewControllers([pageContentViewController!], direction: UIPageViewControllerNavigationDirection.Forward, animated: true, completion: nil)
self.mainPageViewController.view.frame = CGRectMake(0, 102, self.view.frame.width, self.view.frame.height)
self.addChildViewController(mainPageViewController)
self.view.addSubview(mainPageViewController.view)
self.mainPageViewController.didMoveToParentViewController(self)
}
Here I set pageContentVC to the sourveVS of transitionManageClass. NExt how to choose the right GestureRecognizer. By default when you add new gesture recognizer the old one doesnt work. When you disable the new gesture recognizer the old one starts to work! I added new gesture recognizer using code:
var menuViewController: UIViewController! {
didSet {
self.exitPanGesture = UIPanGestureRecognizer()
self.exitPanGesture.addTarget(self, action:"handleOffstagePan:")
// self.exitPanGesture.view?.userInteractionEnabled = false
self.sourceViewController.view.addGestureRecognizer(self.exitPanGesture)
}
}
Before setting menuViewController I set sourceViewController. So, here I am adding new gesture recognizer to my sourceViewController. Next, step is to disable this gesture recognize. When you close the menu disable it using this code:
var presentingP:Bool!{
didSet{
if presentingP == true {
// enable the gesture recognizer only when the view of menucontroller is presented
self.exitPanGesture.view?.userInteractionEnabled = true
}else{
// disable gesture recognizer when menu is not presented
self.exitPanGesture.view?.userInteractionEnabled = false
isMenuVisible = false
}
}
}
PresentingP is boolean value which shows when menu is opened and closed!

How to hide tab bar with animation in iOS?

So I have a button that is connected to a IBAction. When I press the button I want to hide the tab bar in my iOS app with a animation. This [self setTabBarHidden:hidden animated:NO]; or this [self.tabBarController setTabBarHidden:hidden animated:YES]; does not work. This is my code without the animation:
- (IBAction)picture1:(id)sender {
[self.tabBarController.tabBar setHidden:YES];
}
Any help would be greatly appreciated :D
When working with storyboard its easy to setup the View Controller to hide the tabbar on push, on the destination View Controller just select this checkbox:
I try to keep view animations available to me using the following formula:
// pass a param to describe the state change, an animated flag and a completion block matching UIView animations completion
- (void)setTabBarVisible:(BOOL)visible animated:(BOOL)animated completion:(void (^)(BOOL))completion {
// bail if the current state matches the desired state
if ([self tabBarIsVisible] == visible) return (completion)? completion(YES) : nil;
// get a frame calculation ready
CGRect frame = self.tabBarController.tabBar.frame;
CGFloat height = frame.size.height;
CGFloat offsetY = (visible)? -height : height;
// zero duration means no animation
CGFloat duration = (animated)? 0.3 : 0.0;
[UIView animateWithDuration:duration animations:^{
self.tabBarController.tabBar.frame = CGRectOffset(frame, 0, offsetY);
} completion:completion];
}
//Getter to know the current state
- (BOOL)tabBarIsVisible {
return self.tabBarController.tabBar.frame.origin.y < CGRectGetMaxY(self.view.frame);
}
//An illustration of a call to toggle current state
- (IBAction)pressedButton:(id)sender {
[self setTabBarVisible:![self tabBarIsVisible] animated:YES completion:^(BOOL finished) {
NSLog(#"finished");
}];
}
does not longer work on iOS14, see updated 2nde answer below
Swift 3.0 version, using an extension:
extension UITabBarController {
private struct AssociatedKeys {
// Declare a global var to produce a unique address as the assoc object handle
static var orgFrameView: UInt8 = 0
static var movedFrameView: UInt8 = 1
}
var orgFrameView:CGRect? {
get { return objc_getAssociatedObject(self, &AssociatedKeys.orgFrameView) as? CGRect }
set { objc_setAssociatedObject(self, &AssociatedKeys.orgFrameView, newValue, .OBJC_ASSOCIATION_COPY) }
}
var movedFrameView:CGRect? {
get { return objc_getAssociatedObject(self, &AssociatedKeys.movedFrameView) as? CGRect }
set { objc_setAssociatedObject(self, &AssociatedKeys.movedFrameView, newValue, .OBJC_ASSOCIATION_COPY) }
}
override open func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if let movedFrameView = movedFrameView {
view.frame = movedFrameView
}
}
func setTabBarVisible(visible:Bool, animated:Bool) {
//since iOS11 we have to set the background colour to the bar color it seams the navbar seams to get smaller during animation; this visually hides the top empty space...
view.backgroundColor = self.tabBar.barTintColor
// bail if the current state matches the desired state
if (tabBarIsVisible() == visible) { return }
//we should show it
if visible {
tabBar.isHidden = false
UIView.animate(withDuration: animated ? 0.3 : 0.0) {
//restore form or frames
self.view.frame = self.orgFrameView!
//errase the stored locations so that...
self.orgFrameView = nil
self.movedFrameView = nil
//...the layoutIfNeeded() does not move them again!
self.view.layoutIfNeeded()
}
}
//we should hide it
else {
//safe org positions
orgFrameView = view.frame
// get a frame calculation ready
let offsetY = self.tabBar.frame.size.height
movedFrameView = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height + offsetY)
//animate
UIView.animate(withDuration: animated ? 0.3 : 0.0, animations: {
self.view.frame = self.movedFrameView!
self.view.layoutIfNeeded()
}) {
(_) in
self.tabBar.isHidden = true
}
}
}
func tabBarIsVisible() ->Bool {
return orgFrameView == nil
}
}
This is based on the input from Sherwin Zadeh after a few hours of playing around.
Instead of moving the tabbar itself it moves the frame of the view, this effectively slides the tabbar nicely out of the bottom of the screen but...
... has the advantage that the content displayed inside the UITabbarcontroller is then also taking the full screen!
note its also using the AssociatedObject functionality to attached data to the UIView without subclassing and thus an extension is possible (extensions do not allow stored properties)
As per Apple docs, hidesBottomBarWhenPushed property of UIViewController, a Boolean value, indicating whether the toolbar at the bottom of the screen is hidden when the view controller is pushed on to a navigation controller.
The value of this property on the topmost view controller determines whether the toolbar is visible.
The recommended approach to hide tab bar would as follows
ViewController *viewController = [[ViewController alloc] init];
viewController.hidesBottomBarWhenPushed = YES; // This property needs to be set before pushing viewController to the navigationController's stack.
[self.navigationController pushViewController:viewController animated:YES];
However, note this approach will only be applied to respective viewController and will not be propagated to other view controllers unless you start setting the same hidesBottomBarWhenPushed property in other viewControllers before pushing it to the navigation controller's stack.
Swift Version:
#IBAction func tap(sender: AnyObject) {
setTabBarVisible(!tabBarIsVisible(), animated: true, completion: {_ in })
}
// pass a param to describe the state change, an animated flag and a completion block matching UIView animations completion
func setTabBarVisible(visible: Bool, animated: Bool, completion:(Bool)->Void) {
// bail if the current state matches the desired state
if (tabBarIsVisible() == visible) {
return completion(true)
}
// get a frame calculation ready
let height = tabBarController!.tabBar.frame.size.height
let offsetY = (visible ? -height : height)
// zero duration means no animation
let duration = (animated ? 0.3 : 0.0)
UIView.animateWithDuration(duration, animations: {
let frame = self.tabBarController!.tabBar.frame
self.tabBarController!.tabBar.frame = CGRectOffset(frame, 0, offsetY);
}, completion:completion)
}
func tabBarIsVisible() -> Bool {
return tabBarController!.tabBar.frame.origin.y < CGRectGetMaxY(view.frame)
}
[Swift4.2]
Just created an extension for UITabBarController:
import UIKit
extension UITabBarController {
func setTabBarHidden(_ isHidden: Bool, animated: Bool, completion: (() -> Void)? = nil ) {
if (tabBar.isHidden == isHidden) {
completion?()
}
if !isHidden {
tabBar.isHidden = false
}
let height = tabBar.frame.size.height
let offsetY = view.frame.height - (isHidden ? 0 : height)
let duration = (animated ? 0.25 : 0.0)
let frame = CGRect(origin: CGPoint(x: tabBar.frame.minX, y: offsetY), size: tabBar.frame.size)
UIView.animate(withDuration: duration, animations: {
self.tabBar.frame = frame
}) { _ in
self.tabBar.isHidden = isHidden
completion?()
}
}
}
For Xcode 11.3 and iOS 13 other answers didn't work for me. However, based on those I've came up to the new solution using CGAffineTransform
I didn't test this code well, but this might actually work.
extension UITabBarController {
func setTabBarHidden(_ isHidden: Bool) {
if !isHidden { tabBar.isHidden = false }
let height = tabBar.frame.size.height
let offsetY = view.frame.height - (isHidden ? 0 : height)
tabBar.transform = CGAffineTransform(translationX: 0, y: offsetY)
UIView.animate(withDuration: 0.25, animations: {
self.tabBar.transform = .identity
}) { _ in
self.tabBar.isHidden = isHidden
}
}
}
Hope that helps.
UPDATE 09.03.2020:
I've finally found an awesome implementation of hiding tab bar with animation. It's huge advantage it's able to work either in common cases and in custom navigation controller transitions. Since author's blog is quite unstable, I'll leave the code below. Original source: https://www.iamsim.me/hiding-the-uitabbar-of-a-uitabbarcontroller/
Implementation:
extension UITabBarController {
/**
Show or hide the tab bar.
- Parameter hidden: `true` if the bar should be hidden.
- Parameter animated: `true` if the action should be animated.
- Parameter transitionCoordinator: An optional `UIViewControllerTransitionCoordinator` to perform the animation
along side with. For example during a push on a `UINavigationController`.
*/
func setTabBar(
hidden: Bool,
animated: Bool = true,
along transitionCoordinator: UIViewControllerTransitionCoordinator? = nil
) {
guard isTabBarHidden != hidden else { return }
let offsetY = hidden ? tabBar.frame.height : -tabBar.frame.height
let endFrame = tabBar.frame.offsetBy(dx: 0, dy: offsetY)
let vc: UIViewController? = viewControllers?[selectedIndex]
var newInsets: UIEdgeInsets? = vc?.additionalSafeAreaInsets
let originalInsets = newInsets
newInsets?.bottom -= offsetY
/// Helper method for updating child view controller's safe area insets.
func set(childViewController cvc: UIViewController?, additionalSafeArea: UIEdgeInsets) {
cvc?.additionalSafeAreaInsets = additionalSafeArea
cvc?.view.setNeedsLayout()
}
// Update safe area insets for the current view controller before the animation takes place when hiding the bar.
if hidden, let insets = newInsets { set(childViewController: vc, additionalSafeArea: insets) }
guard animated else {
tabBar.frame = endFrame
return
}
// Perform animation with coordinato if one is given. Update safe area insets _after_ the animation is complete,
// if we're showing the tab bar.
weak var tabBarRef = self.tabBar
if let tc = transitionCoordinator {
tc.animateAlongsideTransition(in: self.view, animation: { _ in tabBarRef?.frame = endFrame }) { context in
if !hidden, let insets = context.isCancelled ? originalInsets : newInsets {
set(childViewController: vc, additionalSafeArea: insets)
}
}
} else {
UIView.animate(withDuration: 0.3, animations: { tabBarRef?.frame = endFrame }) { didFinish in
if !hidden, didFinish, let insets = newInsets {
set(childViewController: vc, additionalSafeArea: insets)
}
}
}
}
/// `true` if the tab bar is currently hidden.
var isTabBarHidden: Bool {
return !tabBar.frame.intersects(view.frame)
}
}
If you're dealing with custom navigation transitions just pass a transitionCoordinator property of "from" controller, so animations are in sync:
from.tabBarController?.setTabBar(hidden: true, along: from.transitionCoordinator)
Note, that in such case the initial solution work very glitchy.
I went through the previous posts, so I came out with the solution below as subclass of UITabBarController
Main points are:
Written in Swift 5.1
Xcode 11.3.1
Tested on iOS 13.3
Simulated on iPhone 11 and iPhone 8 (so with and without notch)
Handles the cases where the user taps on the different tabs
Handles the cases where we programmatically change the value of selectedIndex
Handles the view controller orientation changes
Handles the corner casere where the app moved to background and back to foreground
Below the subclass TabBarController:
class TabBarController: UITabBarController {
//MARK: Properties
private(set) var isTabVisible:Bool = true
private var visibleTabBarFrame:CGRect = .zero
private var hiddenTabBarFrame:CGRect = .zero
override var selectedIndex: Int {
didSet { self.updateTabBarFrames() }
}
//MARK: View lifecycle
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.calculateTabBarFrames()
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { (_) in }) { (_) in
// when orientation changes, the tab bar frame changes, so we need to update it to the expected state
self.calculateTabBarFrames()
self.updateTabBarFrames()
}
}
#objc private func appWillEnterForeground(_ notification:Notification){
self.updateTabBarFrames()
}
//MARK: Private
/// Calculates the frames of the tab bar and the expected bounds of the shown view controllers
private func calculateTabBarFrames() {
self.visibleTabBarFrame = self.tabBar.frame
self.hiddenTabBarFrame = CGRect(x: self.visibleTabBarFrame.origin.x, y: self.visibleTabBarFrame.origin.y + self.visibleTabBarFrame.height, width: self.visibleTabBarFrame.width, height: self.visibleTabBarFrame.height)
}
/// Updates the tab bar and shown view controller frames based on the current expected tab bar visibility
/// - Parameter tabIndex: if provided, it will update the view frame of the view controller for this tab bar index
private func updateTabBarFrames(tabIndex:Int? = nil) {
self.tabBar.frame = self.isTabVisible ? self.visibleTabBarFrame : self.hiddenTabBarFrame
if let vc = self.viewControllers?[tabIndex ?? self.selectedIndex] {
vc.additionalSafeAreaInsets.bottom = self.isTabVisible ? 0.0 : -(self.visibleTabBarFrame.height - self.view.safeAreaInsets.bottom)
}
self.view.layoutIfNeeded()
}
//MARK: Public
/// Show/Hide the tab bar
/// - Parameters:
/// - show: whether to show or hide the tab bar
/// - animated: whether the show/hide should be animated or not
func showTabBar(_ show:Bool, animated:Bool = true) {
guard show != self.isTabVisible else { return }
self.isTabVisible = show
guard animated else {
self.tabBar.alpha = show ? 1.0 : 0.0
self.updateTabBarFrames()
return
}
UIView.animate(withDuration: 0.25, delay: 0.0, options: [.beginFromCurrentState,.curveEaseInOut], animations: {
self.tabBar.alpha = show ? 1.0 : 0.0
self.updateTabBarFrames()
}) { (_) in }
}
}
extension TabBarController: UITabBarControllerDelegate {
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
if let tabIndex = self.tabBar.items?.firstIndex(of: item) {
self.updateTabBarFrames(tabIndex: tabIndex)
}
}
}
Sample usage from within a shown view controller:
// hide the tab bar animated (default)
(self.tabBarController as? TabBarController)?.showTabBar(false)
// hide the tab bar without animation
(self.tabBarController as? TabBarController)?.showTabBar(false, animated:false)
Sample output iPhone 11
Sample output iPhone 8
EDIT :
Updated the code to respect the safe area bottom inset
If you're experiencing issues with this solution and your tab bar contains a navigation controller as direct child in the viewControllers array, you may want to make sure that the navigation controller topViewController has the property extendedLayoutIncludesOpaqueBars set to true (you can set this directly from the Storyboard). This should resolve the problem
Hope it helps someone :)
Rewrite Sherwin Zadeh's answer in Swift 4:
/* tab bar hide/show animation */
extension AlbumViewController {
// pass a param to describe the state change, an animated flag and a completion block matching UIView animations completion
func setTabBarVisible(visible: Bool, animated: Bool, completion: ((Bool)->Void)? = nil ) {
// bail if the current state matches the desired state
if (tabBarIsVisible() == visible) {
if let completion = completion {
return completion(true)
}
else {
return
}
}
// get a frame calculation ready
let height = tabBarController!.tabBar.frame.size.height
let offsetY = (visible ? -height : height)
// zero duration means no animation
let duration = (animated ? kFullScreenAnimationTime : 0.0)
UIView.animate(withDuration: duration, animations: {
let frame = self.tabBarController!.tabBar.frame
self.tabBarController!.tabBar.frame = frame.offsetBy(dx: 0, dy: offsetY)
}, completion:completion)
}
func tabBarIsVisible() -> Bool {
return tabBarController!.tabBar.frame.origin.y < view.frame.maxY
}
}
Try to set the frame of the tabBar in animation. See this tutorial.
Just be aware, it's bad practice to do that, you should set show/hide tabBar when UIViewController push by set the property hidesBottomBarWhenPushed to YES.
tried in swift 3.0 / iOS10 / Xcode 8:
self.tabBarController?.tabBar.isHidden = true
I set it when my controller is shown: (and Hide when back, after navigation)
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.tabBarController?.tabBar.isHidden = false
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.tabBarController?.tabBar.isHidden = true
}
BTW: better to have a flag to save if shown or not, as other vents can eventually trigger hide/show
Unfortunately, I can't comment on HixField's answer because I don't have enough reputation, so I have to leave this as a separate answer.
His answer is missing the computed property for movedFrameView, which is:
var movedFrameView:CGRect? {
get { return objc_getAssociatedObject(self, &AssociatedKeys.movedFrameView) as? CGRect }
set { objc_setAssociatedObject(self, &AssociatedKeys.movedFrameView, newValue, .OBJC_ASSOCIATION_COPY) }
}
My previous answer does not longer work on iOS14.
I played with manipulating the frames of the different views, but it seams that the new implementation of the UITabBarController and UITabBar on iOS14 do some magic under the covers which makes this approach no longer working.
I therefore switch to the approach that I hide the UITabBar by setting its alpha to zero and then I manipulate the bottom constraint (that you must pass in when calling the function) to bring the view's content down. This does however, mean that you must have such a constraint and the extension is more bound to your view then the previous approach.
Make sure that the view you are displaying has clipToBounds = false otherwise you will just get a black area where the UITabBar once was!
Here is the code of my UITabBarController.extensions.swift:
import Foundation
extension UITabBarController {
private struct AssociatedKeys {
// Declare a global var to produce a unique address as the assoc object handle
static var orgConstraintConstant: UInt8 = 0
static var orgTabBarAlpha : UInt8 = 1
}
var orgConstraintConstant: CGFloat? {
get { return objc_getAssociatedObject(self, &AssociatedKeys.orgConstraintConstant) as? CGFloat }
set { objc_setAssociatedObject(self, &AssociatedKeys.orgConstraintConstant, newValue, .OBJC_ASSOCIATION_COPY) }
}
var orgTabBarAlpha: CGFloat? {
get { return objc_getAssociatedObject(self, &AssociatedKeys.orgTabBarAlpha) as? CGFloat }
set { objc_setAssociatedObject(self, &AssociatedKeys.orgTabBarAlpha, newValue, .OBJC_ASSOCIATION_COPY) }
}
func setTabBarVisible(visible:Bool, animated:Bool, bottomConstraint: NSLayoutConstraint) {
// bail if the current state matches the desired state
if (tabBarIsVisible() == visible) { return }
//define segment animation duration (note we have two segments so total animation time = times 2x)
let segmentAnimationDuration = animated ? 0.15 : 0.0
//we should show it
if visible {
//animate moving up
UIView.animate(withDuration: segmentAnimationDuration,
delay: 0,
options: [],
animations: {
[weak self] in
guard let self = self else { return }
bottomConstraint.constant = self.orgConstraintConstant ?? 0
self.view.layoutIfNeeded()
},
completion: {
(_) in
//animate tabbar fade in
UIView.animate(withDuration: segmentAnimationDuration) {
[weak self] in
guard let self = self else { return }
self.tabBar.alpha = self.orgTabBarAlpha ?? 0
self.view.layoutIfNeeded()
}
})
//reset our values
self.orgConstraintConstant = nil
}
//we should hide it
else {
//save our previous values
self.orgConstraintConstant = bottomConstraint.constant
self.orgTabBarAlpha = tabBar.alpha
//animate fade bar out
UIView.animate(withDuration: segmentAnimationDuration,
delay: 0,
options: [],
animations: {
[weak self] in
guard let self = self else { return }
self.tabBar.alpha = 0.0
self.view.layoutIfNeeded()
},
completion: {
(_) in
//then animate moving down
UIView.animate(withDuration: segmentAnimationDuration) {
[weak self] in
guard let self = self else { return }
bottomConstraint.constant = bottomConstraint.constant - self.tabBar.frame.height + 4 // + 4 looks nicer on no-home button devices
//self.tabBar.alpha = 0.0
self.view.layoutIfNeeded()
}
})
}
}
func tabBarIsVisible() ->Bool {
return orgConstraintConstant == nil
}
}
This is how it looks in my app (you can compare to my 1ste answer, the animation is a bit different but looks great) :
You can have a bug when animating manually the tab bar on iOS13 and Xcode 11. If the user press the home button after the animation (it'll just ignore the animation and will be there in the right place). I think it's a good idea to invert the animation before that by listening to the applicationWillResignActive event.
This wrks for me:
[self.tabBar setHidden:YES];
where self is the view controller, tabBar is the id for the tabBar.

Resources