如何在iOS中使用动画隐藏选项卡栏?

69

我有一个与IBAction相关联的按钮。当我按下这个按钮时,我想用动画隐藏我的iOS应用程序中的选项卡栏。但是,这个 [self setTabBarHidden:hidden animated:NO]; 或者这个 [self.tabBarController setTabBarHidden:hidden animated:YES]; 都无法实现。这是没有动画效果的代码:

- (IBAction)picture1:(id)sender {
    [self.tabBarController.tabBar setHidden:YES];
}

希望能得到任何帮助,非常感激 :D


Xcode 中有很多选项卡栏,你想隐藏哪一个? - user529758
(或者,你是想在你的iOS应用程序中隐藏选项卡栏,巧合吗?如果是这样,那么为什么要标记Xcode,而与Xcode无关呢?) - user529758
@H2CO3 显然,该OP试图在他们的iOS应用程序中隐藏UITabBarController,而不是Xcode。他们只是错标了标签。 - Sam Spencer
@Ben 不要请求将你的答案标记为已接受,特别是当另一个获得更多赞同的答案已经被标记为已接受时。决定哪个答案被接受是由发布问题的人决定的。 - neilco
从2018年开始,Swift 4.x(抱歉关于通知),看到了这个要点。https://gist.github.com/simme/a44cd16f89038cbee8537b89d237386b 这是我能找到的最好的解决方案。然而,当你旋转应用程序时,tabBarController的tabBar将重置。 - Glenn Posadas
15个回答

105

在使用Storyboard时,设置视图控制器在推动时隐藏选项卡栏很容易,在目标视图控制器上只需选择此复选框:
enter image description here


4
为什么这个还没有被认为是正确的呢?我会标记它,它肯定是最干净的方式! - dulan
6
如果您想在推动时隐藏内容,这就是正确的方法。对于我们的应用程序,当用户在同一视图控制器内点击屏幕时需要隐藏该栏,因此这种方法不适用。 - ftvs
1
@ftvs 对于你的情况,你应该使用“代码内”选项。 - Ben
8
这不是问题要求的。他想在按钮点击时隐藏选项卡栏。 - tupakapoor
5
这个属性在UITabBarController中嵌套UINavigationController和UISplitViewController时无法正常工作:http://www.openradar.me/24846972 - jamesk
注意:从iOS 14开始,此解决方案不再适用!我正在寻找修复方法,但UITabbar(控制器)的行为似乎违反所有逻辑... :-/ - HixField

85

我尝试使用以下公式使视图动画对我可用:

// 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");
    }];
}

7
太棒了!我将其翻译成Swift语言,以回答我的问题:https://dev59.com/8F8d5IYBdhLWcg3wRAeE#27072876 - Michael Campsall
这是完美的。非常适合作为一个类别的候选项。 - jesses.co.tt
1
在调用 return completion(YES) 之前,您需要对 completion 进行 nil 检查。 - jszumski
@jszumski - 当传递的块是可选的时,需要进行nil检查。但你说得对,在这种情况下,可选完成块是有意义的。请参见编辑。 - danh

41

不再支持iOS14,请见下面更新的第二个答案

使用扩展的Swift 3.0版本:

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
    }
}
  • 这是基于 Sherwin Zadeh 几小时的尝试后得出的结论。
  • 与其移动标签栏本身,它移动了视图的框架,这有效地将选项卡栏精美地滑出屏幕底部,但...
  • ... 优点在于 UITabBarController 中显示的内容也可以占据整个屏幕!
  • 请注意,它还使用 AssociatedObject 功能将数据附加到 UIView 而不需要子类化,从而可以进行扩展(扩展不允许存储属性)。

enter image description here


1
很棒的解决方案! - tarmes
像魔法一样运作。 - MilanPanchal
4
自iOS 11以来,我们必须将背景颜色设置为导航栏颜色。在动画期间,导航栏似乎变小了,这样可以视觉上隐藏顶部的空白区域。 - HixField
这个有没有 Objective-C 版本? - daris mathew
@yoninja 你是如何为 iPhone X 解决这个问题的? - Tommy Sadiq Hinrichsen
显示剩余12条评论

32

根据苹果文档,UIViewController的hidesBottomBarWhenPushed属性是一个布尔值,指示当视图控制器被推到导航控制器上时屏幕底部的工具栏是否隐藏。

顶层视图控制器上此属性的值决定了工具栏是否可见。

建议隐藏选项卡栏的方法如下:

    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];

然而,请注意这种方法只适用于各自的视图控制器,并且除非在将其推入导航控制器堆栈之前在其他视图控制器中设置相同的hidesBottomBarWhenPushed属性,否则不会传播到其他视图控制器。


2
这个答案是不正确的,因为该属性并不决定工具栏是否可见。如果工具栏已经被隐藏,将此属性设置为false并不能显示它。 - Daniel T.

10

Swift 版本:

@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)
}

8

[Swift4.2]

刚刚为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?()
        }
    }
}


7

对于 Xcode 11.3 和 iOS 13,其他答案对我没有用。但是,基于那些答案,我想到了使用 CGAffineTransform 的新解决方案。

我没有很好地测试过这个代码,但它可能真的有效。

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
        }
    }

}

希望这能有所帮助。
更新于09.03.2020:
我终于找到了一个很棒的隐藏选项卡栏的实现方式,它的优点在于既可以在常见情况下工作,也可以在自定义导航控制器转换中工作。由于作者的博客相当不稳定,我将在下面留下代码。原始来源:https://www.iamsim.me/hiding-the-uitabbar-of-a-uitabbarcontroller/ 实现:
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)
    }

}

如果你正在处理自定义导航转场,只需传递“来自”控制器的transitionCoordinator属性即可使动画同步:

from.tabBarController?.setTabBar(hidden: true, along: from.transitionCoordinator)

请注意,这种情况下,初始解决方案会非常不稳定。

6

我查看了之前的帖子,因此我提出以下解决方案作为UITabBarController的子类。

主要观点如下:

  • 使用Swift 5.1编写
  • Xcode 11.3.1
  • iOS 13.3上进行测试
  • iPhone 11iPhone 8上进行模拟(因此具有或不具有缺口)
  • 处理用户点击不同选项卡的情况
  • 处理程序编程更改selectedIndex值的情况
  • 处理视图控制器方向更改的情况
  • 处理应用程序移动到后台并返回前台的角落情况

以下是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)
        }
    }
}

在已显示的视图控制器中,以下是示例用法:

// 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)

iPhone 11示例输出

iPhone 11示例

iPhone 8示例输出

iPhone 8示例

编辑: :

  • 更新代码以尊重安全区域底部插图
  • 如果您在使用此解决方案时遇到问题,并且您的选项卡栏包含导航控制器作为 viewControllers 数组中的直接子级,则可能需要确保导航控制器 topViewController 具有属性 extendedLayoutIncludesOpaqueBars 设置为 true (可以直接从Storyboard设置)。这应该解决问题

希望能对某些人有所帮助 :)


iPhone X上的错误行为 - Дмитрий Акимов

5

用Swift 4重写Sherwin Zadeh的答案:

/* 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
    }
}

1
我认为 tabBarIsVisible 函数在 iOS13 / xcode11 上不起作用。 - Elano Vasconcelos

4
尝试在动画中设置 tabBar 的框架。请参阅此教程:这里
但是需要注意的是,这样做是不好的实践。您应该通过将属性 hidesBottomBarWhenPushed 设置为 YES 来在 UIViewController 推送时显示/隐藏 tabBar。

hidesBottomBarWhenPushed不会隐藏标签栏。底部栏是可选的工具栏,位于视图底部,适用于作为导航控制器子级的视图控制器。 - Matthias Bauch
@MatthiasBauch,如果视图控制器在UINavigationController中,则实际上会隐藏UITabBarController的tabBar。我很惊讶。文档说这是用于导航控制器的底部栏。 - Michael McGuire
是的,没错。几周前我就已经弄清楚了。如果我没有编译代码,我可能会出丑,因为我想告诉同事这行不通。苹果的文档在这方面相当不清楚。 - Matthias Bauch
1
UINavigationController.h 中的这个注释揭示了真相:public var hidesBottomBarWhenPushed: Bool // 如果为 YES,则当此视图控制器被推入具有底部栏(如选项卡栏)的控制器层次结构时,底部栏将滑出。默认值为 NO。 - jamesk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接