Как скрыть панель вкладок с анимацией в 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.