Как скрыть панель вкладок с анимацией в iOS?

Так что у меня есть кнопка, которая связана с IBAction. Когда я нажимаю кнопку, я хочу скрыть панель вкладок в моем приложении для iOS с анимацией. это [self setTabBarHidden:hidden animated:NO]; или это [self.tabBarController setTabBarHidden:hidden animated:YES]; не работает. Это мой код без анимации:

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

Любая помощь будет принята с благодарностью:D

11 ответов

Решение

Я стараюсь держать анимации просмотра доступными для меня, используя следующую формулу:

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

При работе с раскадровкой легко настроить View Controller, чтобы скрыть панель вкладок при нажатии, в целевом View Controller просто установите этот флажок:

Версия 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
    }
}
  • Это основано на вкладе Шервина Заде после нескольких часов игры.
  • Вместо того, чтобы перемещать саму панель вкладок, она перемещает рамку представления, это эффективно перемещает панель вкладок из нижней части экрана, но...
  • ... имеет то преимущество, что содержимое, отображаемое внутри UITabbarcontroller, также занимает весь экран!
  • обратите внимание, что он также использует функциональные возможности AssociatedObject для присоединения данных к UIView без создания подклассов, и, следовательно, расширение возможно (расширения не позволяют сохранять свойства)

Согласно документам Apple, свойство hidesBottomBarWhenPhed объекта UIViewController представляет собой логическое значение, указывающее, скрыта ли панель инструментов в нижней части экрана, когда контроллер представления помещается в контроллер навигации.

Значение этого свойства в самом верхнем контроллере представления определяет, видна ли панель инструментов.

Рекомендуемый подход к скрытию панели вкладок будет следующим

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

Однако обратите внимание, что этот подход будет применяться только к соответствующему viewController и не будет распространяться на другие контроллеры представления, если только вы не начнете устанавливать то же свойство hidesBottomBarWhenPressed в других viewControllers, прежде чем помещать его в стек контроллера навигации.

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

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

Для 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" контроллера, поэтому анимации синхронизируются:

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

Обратите внимание, что в этом случае исходное решение работает очень некорректно.

Я просмотрел предыдущие сообщения, поэтому я предложил решение ниже как подкласс UITabBarController

Основные моменты:

  • Написано на Swift 5.1
  • Xcode 11.3.1
  • Проверено на iOS 13.3
  • Имитация на iPhone 11 и iPhone 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 8

ИЗМЕНИТЬ:

  • Обновлен код с учетом нижней вставки в безопасной зоне.
  • Если у вас возникли проблемы с этим решением и ваша панель вкладок содержит контроллер навигации как прямой дочерний элемент в viewControllers массив, вы можете убедиться, что контроллер навигации topViewController имеет свойство extendedLayoutIncludesOpaqueBars установлен в true(вы можете установить это прямо из раскадровки). Это должно решить проблему

Надеюсь, это кому-то поможет:)

Перепишите ответ Шервина Заде в 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
    }
}

Попробуйте установить рамку вкладки в анимации. Смотрите этот урок.

Просто знайте, это плохая практика, вы должны установить show/hide tabBar, когда UIViewController нажмите, чтобы установить свойство hidesBottomBarWhenPushed в YES,

Попробовал в swift 3.0 / iOS10 / Xcode 8:

    self.tabBarController?.tabBar.isHidden = true

Я установил его, когда отображается мой контроллер: (и Скрыть, когда вернулся, после навигации)

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

    }

Кстати: лучше иметь флаг для сохранения, если он показан или нет, так как другие отверстия могут в конечном итоге вызвать скрытие / показ

Мой предыдущий ответ больше не работает на iOS14. Я играл с манипулированием фреймами различных представлений, но похоже, что новая реализация UITabBarController и UITabBar на iOS14 творит чудеса, из-за которых этот подход больше не работает.

Поэтому я переключаюсь на подход, в котором я скрываю UITabBar, устанавливая его альфа равным нулю, а затем манипулирую нижним ограничением (которое вы должны передать при вызове функции), чтобы уменьшить содержимое представления. Однако это означает, что у вас должно быть такое ограничение, и расширение больше привязано к вашему представлению, чем предыдущий подход.

Убедитесь, что отображаемое представление имеет clipToBounds = false, иначе вы просто получите черную область на месте UITabBar!

Вот код моего 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
    }
}

Вот как это выглядит в моем приложении (вы можете сравнить с моим ответом 1ste, анимация немного другая, но выглядит великолепно):

К сожалению, я не могу комментировать ответ HixField, потому что у меня недостаточно репутации, поэтому я должен оставить это как отдельный ответ.

В его ответе отсутствует вычисленное свойство для movedFrameView, который:

var movedFrameView:CGRect? {
  get { return objc_getAssociatedObject(self, &AssociatedKeys.movedFrameView) as? CGRect }
  set { objc_setAssociatedObject(self, &AssociatedKeys.movedFrameView, newValue, .OBJC_ASSOCIATION_COPY) }
}

У вас может быть ошибка при ручной анимации панели вкладок в iOS13 и Xcode 11. Если пользователь нажимает кнопку "Домой" после анимации (он просто проигнорирует анимацию и будет там в нужном месте). Я думаю, что перед этим лучше всего инвертировать анимацию, слушая событие applicationWillResignActive.

Основываясь на ответах Хиксфилда и Била Чана, я подготовил версию, которая работает на iOS 9 - iOS 11 (сейчас я не могу тестировать другие версии), а также iPhone 4S на iPhone X (проверено). Если вы хотите скрыть панель вкладок, просто позвоните: tabBarController?.set(visible:false, animated: true)Аналогично, если вы хотите показать вызов: tabBarController?.set(visible:true, animated: true), Вы также можете найти этот код в моей суть: https://gist.github.com/MaciejGad/9a4d1f65dcf382373911c90c548d2713

extension UITabBarController {

func set(visible: Bool, animated: Bool, completion: ((Bool)->Void)? = nil ) {

    guard isVisible() != visible else {
        completion?(true)
        return
    }

    let offsetY = tabBar.frame.size.height
    let duration = (animated ? 0.3 : 0.0)

    let beginTransform:CGAffineTransform
    let endTransform:CGAffineTransform

    if visible {
        beginTransform = CGAffineTransform(translationX: 0, y: offsetY)
        endTransform = CGAffineTransform.identity
    } else {
        beginTransform = CGAffineTransform.identity
        endTransform = CGAffineTransform(translationX: 0, y: offsetY)
    }

    tabBar.transform = beginTransform
    if visible {
        tabBar.isHidden = false
    }

    UIView.animate(withDuration: duration, animations: {
        self.tabBar.transform = endTransform
    }, completion: { compete in
        completion?(compete)
        if !visible {
            self.tabBar.isHidden = true
        }
    })
}

func isVisible() -> Bool {
    return !tabBar.isHidden
}
}

Это работает для меня:[self.tabBar setHidden:YES];
где self - контроллер представления, tabBar - идентификатор для tabBar.

Другие вопросы по тегам