I am trying to Flip two UIViews. I've try to flip UIView using programmatically and it works perfect. But when i've try to flip UIView that i created in storyboard it not works, First time it flip UIView but second time it flip blank UiViews? Any one have any idea is there any mistake in my code?
In this picture Top left Debug view Hierarchy picture is before animating button and bottom left Debug view Hierarchy picture is after animating picture.
When second time i animate the UIView it Flip like this below picture.
class ViewController: UIViewController {
#IBOutlet var container: UIView!
#IBOutlet var blueSquare : UIView!
#IBOutlet var redSquare : UIView!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func animateButtonTapped(sender: AnyObject) {
// create a 'tuple' (a pair or more of objects assigned to a single variable)
var views : (frontView: UIView, backView: UIView)
if ((self.redSquare.superview) != nil) {
views = (frontView: self.redSquare, backView: self.blueSquare)
}
else {
views = (frontView: self.blueSquare, backView: self.redSquare)
}
// set a transition style
let transitionOptions = UIViewAnimationOptions.TransitionFlipFromLeft
// with no animation block, and a completion block set to 'nil' this makes a single line of code
UIView.transitionFromView(views.frontView, toView: views.backView, duration: 1.0, options: transitionOptions, completion: nil)
}
}
Programmatically
That code is perfectly works.
let container = UIView()
let redSquare = UIView()
let blueSquare = UIView()
override func viewDidLoad() {
super.viewDidLoad()
// set container frame and add to the screen
self.container.frame = CGRect(x: 60, y: 60, width: 200, height: 200)
self.view.addSubview(container)
// set red square frame up
// we want the blue square to have the same position as redSquare
// so lets just reuse blueSquare.frame
self.redSquare.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
self.blueSquare.frame = redSquare.frame
// set background colors
self.redSquare.backgroundColor = UIColor.redColor()
self.blueSquare.backgroundColor = UIColor.blueColor()
// for now just add the redSquare
// we'll add blueSquare as part of the transition animation
self.container.addSubview(self.redSquare)
}
#IBAction func animateButtonTapped(sender: AnyObject) {
// create a 'tuple' (a pair or more of objects assigned to a single variable)
var views : (frontView: UIView, backView: UIView)
if((self.redSquare.superview) != nil){
views = (frontView: self.redSquare, backView: self.blueSquare)
}
else {
views = (frontView: self.blueSquare, backView: self.redSquare)
}
// set a transition style
let transitionOptions = UIViewAnimationOptions.TransitionFlipFromLeft
// with no animation block, and a completion block set to 'nil' this makes a single line of code
UIView.transitionFromView(views.frontView, toView: views.backView, duration: 1.0, options: transitionOptions, completion: nil)
}
UPDATE
var check = true
#IBAction func animateButtonTapped(sender: AnyObject) {
// create a 'tuple' (a pair or more of objects assigned to a single variable)
var views : (frontView: UIView, backView: UIView)
if (check == true) {
views = (frontView: self.redSquare, backView: self.blueSquare)
check = false
}
else {
views = (frontView: self.blueSquare, backView: self.redSquare)
check = true
}
// set a transition style
let transitionOptions : UIViewAnimationOptions = [UIViewAnimationOptions.TransitionFlipFromLeft, UIViewAnimationOptions.ShowHideTransitionViews]
// with no animation block, and a completion block set to 'nil' this makes a single line of code
UIView.transitionFromView(views.frontView, toView: views.backView, duration: 1.0, options: transitionOptions, completion: nil)
}
The default behaviour for transitionFromView removes the view after animation.
let transitionOptions: UIViewAnimationOptions = [.TransitionFlipFromLeft, .ShowHideTransitionViews]
From the documentation: https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIView_Class/#//apple_ref/swift/struct/c:#E#UIViewAnimationOptions
ShowHideTransitionViews
When present, this key causes views to be hidden or shown (instead of removed or added) when performing a view transition. Both views must already be present in the parent view’s hierarchy when using this key. If this key is not present, the to-view in a transition is added to, and the from-view is removed from, the parent view’s list of subviews.
Ah in storyboard I see that you add both views to the container but in code you only add the redsquare. Perhaps remove the bluesquare from being within the container in storyboard?
I am trying to animate the root-view-controller-change in my app. After I swap the view controllers, I load the data necessary for the 2nd controller right away. While the data is loading, I show a loader(MBProgressHUD). This is my function for swapping the view controllers:
class ViewUtils {
class func animateRootViewController(duration: NSTimeInterval, changeToViewController: UIViewController) {
let window = UIApplication.sharedApplication().delegate?.window?
if window == nil {
return
}
UIView.transitionWithView(window!,
duration: duration,
options: UIViewAnimationOptions.TransitionFlipFromLeft | UIViewAnimationOptions.AllowAnimatedContent,
animations: {
window!.rootViewController = changeToViewController
},
completion: nil
)
}
}
All good with this but one thing - it totally breaks the loader. I am attaching an imagine of what's happening:
This is the 2nd view controller while rotating. Once the rotation is complete, the loader appears just fine, both the spinner and the text tween to the correct position in the rounded rectangle.
I really don't understand why this happens, would somebody explain it to me, please? Is there a way to prevent it?
The code of the 2nd view controller where I show the loader:
override func viewDidLoad() {
super.viewDidLoad()
hud = HUD(containingView: view)
hud.show()
createBackground()
}
And my hud class:
class HUD {
private var hudBG: UIView!
private var view: UIView!
private(set) var isShown = false
init(containingView: UIView) {
view = containingView
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func show() {
if !isShown {
if(hudBG == nil) {
hudBG = UIView(frame: CGRectMake(0, 0, view.bounds.width, view.bounds.height))
hudBG.backgroundColor = UIColor(white: 0, alpha: 0.4)
}
view.addSubview(hudBG)
let hud = MBProgressHUD.showHUDAddedTo(view, animated: true)
hud.mode = MBProgressHUDModeIndeterminate
hud.labelText = "Cargando"
hudBG.alpha = 0
UIView.animateWithDuration(0.3, animations: { () -> Void in
self.hudBG.alpha = 1
})
isShown = true
}
}
func hide() {
if isShown {
UIView.animateWithDuration(0.3, animations: {
() -> Void in
self.hudBG.alpha = 0
}, completion: {
(b) -> Void in
self.hudBG.removeFromSuperview()
})
MBProgressHUD.hideHUDForView(view, animated: true)
isShown = false
}
}
}
Thanks a lot for any ideas!
You are adding the hud to a view that is not properly initialized yet.
If you are loading the view controller from a xib or storyboard, the view and it's subviews have the size as they were loaded from interface.
You have to add the hud after the views have been resized to their final size.
If you move
hud = HUD(containingView: view)
hud.show()
to viewDidLayoutSubviews, it should work fine.
I noticed a similar problem when moving an app from iOS 7 to iOS 8. During animations, especially when scaling was involved, the view positions got distorted.
I am pretty sure it's a bug. The simplest workaround is to animate only screenshots or view snapshots, not actual views - it's more work and you can't have views animating when the main animation is in progress but in general it's a more stable solution.
i want to create a slide out menu which has a normal width of about 50 pixels, and if the user press the expand button i will also show the labels for the button.
Like in this example:
What is the correct way to create such a menu? I though about using 2 views and set the size of contentview to width-50pixels.
But i am unable to change the frame of my UIView in the ViewDidLoad function. (this is an example)
override func viewDidLoad() {
super.viewDidLoad()
self.view.frame = CGRect(x:50, y:0, width:974, height:768)
sidebarIsOpen = false
}
And if the user click on the expand Button
#IBAction func expandButtonClicked(sender : AnyObject) {
var x = self.sidebarIsOpen! ? 50 : 300
UIView.animateWithDuration(0.2, animations: {
self.view.frame = CGRect(x:x, y:0, width:300, height:768)
}, completion: { _ in
self.sidebarIsOpen = !(self.sidebarIsOpen!)
})
}
If i click the button again, everything is fine. But on ViewDidLoad i am unable to move the contentview to right.
Thanks in advance
Ill found already the solution by myself.
Created 2 views and animate the "contentview" on button click.
#IBOutlet weak var menuView: UIView!
#IBAction func toggleMenuButton(sender: UIBarButtonItem) {
var x = self.sidebarIsOpen! ? 50 : 200
UIView.animateWithDuration(0.2, animations: {
self.contentView.frame = CGRect(x:x, y:0, width:974, height:768)
}, completion: { _ in
self.sidebarIsOpen = !(self.sidebarIsOpen!)
})
}
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.