Swift - pageviewcontroller tableview in scrollview using snapkit - ios

I just faced a really difficult point while making a complicated view that has pageViewController, tableview, and scrollview.
This is what I want to program
case 1) scroll up : button stactview and pageviewcontroller locate headerview.snp.botttom and hide red view and tableview will scroll
case 2) scroll down : show red view, button stactview and pageviewcontroller locate red view.snp.bottom (like photo)
I made it like these cases, but the problem is tableview in pageviewcontroller didn't move.
What should I do to make it smooth scrolling?
I found example what I want to make
https://raw.githubusercontent.com/tsucres/HeaderedTabScrollView/master/Screenshots/customAnim.gif
http://cocoadocs.org/docsets/MXSegmentedPager/3.2.2/
I build view programmatically using snapkit
self.tabTopValue = 440 // redview height
self.view.addSubview(scrollView)
scrollView.snp.makeConstraints { make in
make.leading.trailing.bottom.equalToSuperview()
make.top.equalTo(headerView.snp.bottom)
}
scrollView.addSubview(contentView)
contentView.snp.makeConstraints { make in
make.edges.equalToSuperview()
make.width.equalTo(self.scrollView)
}
redView.snp.makeConstraints { make in
make.leading.equalTo(contentView.snp.leading)
make.trailing.equalTo(contentView.snp.trailing)
make.top.equalTo(header.snp.bottom).offset(0)
make.height.equalTo(440)
}
// tabbar
tabButtonStackView.addArrangedSubview(historyInfoButton)
tabButtonStackView.addArrangedSubview(graphButton)
tabButtonStackView.addArrangedSubview(lineageInfoButton)
contentView.addSubview(tabButtonStackView)
tabButtonStackView.snp.makeConstraints { make in
make.leading.trailing.equalTo(contentView)
tabTopConstraint = make.top.equalTo(headerView.snp.bottom).offset(self.tabTopValue).constraint
make.height.equalTo(40)
}
addChild(pageViewController)
contentView.addSubview(pageViewController.view)
pageViewController.didMove(toParent: self)
pageViewController.view.snp.makeConstraints { make in
make.top.equalTo(tabButtonStackView.snp.bottom)
make.leading.trailing.equalTo(contentView)
make.height.equalTo(scrollView)
make.bottom.equalTo(contentView.snp.bottom).offset(0)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print("detail scroll = \(scrollView.contentOffset.y)")
if scrollView.contentOffset.y < 0 {
self.tabButtonStackView.snp.updateConstraints { make in
self.tabTopConstraint = make.top.equalTo(headerView.snp.bottom).offset(scrollView.contentOffset.y + tabTopValue).constraint
}
} else if scrollView.contentOffset.y == 0 {
UIView.animate(withDuration: 0.3, delay: 0.0, animations: {
self.tabButtonStackView.snp.updateConstraints { make in
self.tabTopConstraint = make.top.equalTo(self.headerView.snp.bottom).offset(self.tabTopValue).constraint
}
}, completion: nil)
}
if scrollView.contentOffset.y < tabTopValue {
self.tabButtonStackView.snp.updateConstraints { make in
self.tabTopConstraint = make.top.equalTo(headerView.snp.bottom).offset(tabTopValue - scrollView.contentOffset.y).constraint
}
} else if scrollView.contentOffset.y > tabTopValue {
self.tabButtonStackView.snp.updateConstraints { make in
self.tabTopConstraint = make.top.equalTo(headerView.snp.bottom).offset(0).constraint
}
}
}

Related

use UIScrollView to show CarouselImagesView

I have a UIScrollView can show a set of pages and have "carousel" effect, what I want is snap to an image after the user finishes dragging.Should I add some other func such like scrollViewDidEndDragging to observe user's action??? Thank you.
Code
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
timer?.invalidate()
scrolled(scrollView: scrollView)
}
private func scrolled(scrollView: UIScrollView) {
guard let imageUrls = self.imageUrls else {
return
}
if imageUrls.count < 1 {
return
}
.
if abs(lastContentOffset - scrollView.contentOffset.x) < 50 {
return
}
if lastContentOffset > scrollView.contentOffset.x {
if currentPageIndex == 0 {
currentPageIndex = imageUrls.count - 1
} else {
currentPageIndex -= 1
}
} else {
if currentPageIndex < imageUrls.count - 1 {
currentPageIndex += 1
} else {
currentPageIndex = 0
}
}
layoutImages()
self.delegate?.didChangePage(currentPageIndex)
}
First of all scroll view are not reliable from memory point of view, in future if there are lots of images loaded dynamically your app may end up in memory flood situation and may crash.
Better use views like collection view, stack view or table view for cell reusability and it will also solve dragging problem.

Having trouble with LargeTitle and a segmented control with a table view

Sample project can be found at https://github.com/SRowley90/LargeTitleIssueTestiOS
I am trying to position a segmented control below the Large title in an iOS app. I have a UIToolbar which contains the segmented control inside.
When scrolling up the title and toolbar behave as expected.
When scrolling down the navigation bar is correct, but it doesn't push the UITabBar or the UITableView down, meaning the title goes above the segmented control as can be seen in the images below.
I'm pretty sure it's something to do with the constraints I have set, but I can't figure out what.
The TabBar is fixed to the top, left and right.
The TableView is fixed to the bottom, left and right.
The tableView is fixed vertically to the TabBar
I have the position UITabBarDelegate method set:
func position(for bar: UIBarPositioning) -> UIBarPosition {
return .topAttached
}
Take the delegation of the tableView somewhere:
tableView.delegate = self
Override the scrollViewDidScroll and update toolbar position appearance (since the real position should not change according to have that nice bounce effect.
extension ViewController: UIScrollViewDelegate {
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
var verticalOffset = scrollView.contentOffset.y + defaultNavigationBarHeight
if scrollView.refreshControl?.isRefreshing ?? false {
verticalOffset += 60 // After is refreshing changes its value the toolbar goes 60 points down
print(toolbar.frame.origin.y)
}
if verticalOffset >= 0 {
toolbar.transform = .identity
} else {
toolbar.transform = CGAffineTransform(translationX: 0, y: -verticalOffset)
}
}
}
You can use the following check before applying transformation to make it more reliable and natural to default iOS style:
if #available(iOS 11.0, *) {
guard let navigationController = navigationController else { return }
guard navigationController.navigationBar.prefersLargeTitles else { return }
guard navigationController.navigationItem.largeTitleDisplayMode != .never else { return }
}
Using UIScrollViewDelegate didn't work well with CollectionView and toolbar for me. So, I did:
final class CollectionViewController: UICollectionViewController {
private var observesBag: [NSKeyValueObservation] = []
private let toolbar = UIToolbar()
override func viewDidLoad() {
super.viewDidLoad()
let statusBarHeight = UIApplication.shared.statusBarFrame.height
let navigationBarHeight = navigationController?.navigationBar.frame.height ?? 0
let defaultNavigationBarHeight = statusBarHeight + navigationBarHeight
let observation = navigationController!
.navigationBar
.observe(\.center, options: NSKeyValueObservingOptions.new) { [weak self] navBar, _ in
guard let self = self else { return }
let newNavigatonBarHeight = navBar.frame.height + statusBarHeight
let yTranslantion = newNavigatonBarHeight - defaultNavigationBarHeight
if yTranslantion > 0 {
self.toolbar.transform = CGAffineTransform(
translationX: 0,
y: yTranslantion
)
} else {
self.toolbar.transform = .identity
}
}
observesBag.append(observation)
}
}
Observe the "center" of the navigationBar for changes and then translate the toolbar in the y-axis.
Even though it worked fine when I tried to use this solution with UIRefreshControl and Large Titles it didn't work well.
I set up the refresh control like:
private func setupRefreshControl() {
let refreshControl = UIRefreshControl()
self.webView.scrollView.refreshControl = refreshControl
}
the height of the UINavigationBar is changed after the complete refresh triggers.

How to move view down by animation?

In my app, I implement webview, and under that webview I have a one view and I want to move that view by animation like in Safari.
Here is my code:
func scrollViewWillBeginDragging(scrollView: UIScrollView) {
if scrollView.panGestureRecognizer.translationInView(scrollView.superview).y > 0 {
// scrolls down
print("UP")
viewbottom.hidden = false
viewHieght.constant = 45
} else {
print("DOWN")
viewbottom.hidden = true
viewHieght.constant = 0
}
}
In this code I am hiding the view while scroll down but I want to move it down slowly like Safari. So how can I do this?
use layoutIfNeeded() property with animateWithDuration
UIView.animateWithDuration(0.2, animations: { () -> Void in
viewHieght.constant = 45
self.view.layoutIfNeeded()
})
viewHeight.constant = max(0, min(45, scrollView.panGestureRecognizer.translationInView(scrollView.superview).y))
This should make the view move with the gesture but min be 0 and max be 45

Material "pull to refresh" in Swift?

I am trying to achieve pull to refresh in a WKWebView like in this Material pull to refresh GIF. Because we want to include websites that already have a html navigation bar, we need to keep the web view fix when dragging down. I found pull to refresh (UIRefreshControl) to table views and web views but the views go down as the user drags down.
I set the delegate to scrollView property of the web view and receive notifications. When I drag near to top and get to 0 (vertical scroll view offset) I can disable the scroll view, enable the pan gesture. But to actually move the custom spinner view I need a second touch.
The method func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) doesn't work because I have a scroll view over.
private func gestures() {
self.panGesture = UIPanGestureRecognizer(
target: self,
action: "panGestureCaptured:"
)
self.panGesture!.enabled = false
self.webView.addGestureRecognizer(self.panGesture!)
}
func scrollViewDidScroll(scrollView: UIScrollView) {
if scrollView.contentOffset.y <= 0 {
self.view.layoutIfNeeded()
self.spinnerTopLayoutConstraint?.constant = -scrollView.contentOffset.y
}
}
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if scrollView.panGestureRecognizer.translationInView(scrollView.superview).y > 0 { // dragging down
if scrollView.contentOffset.y == 0 {
self.webView!.scrollView.scrollEnabled = false
// self.webView!.scrollView.canCancelContentTouches = true
self.panGesture!.enabled = true
// self.refreshWebView()
}
self.showNavigationItems()
} else { // dragging up
self.hideNavigationItems()
}
}
Thanks to #Darko: the idea was to use the panGesture property of the scroll view (also property of the web view).
private func gestures() {
self.webView.scrollView.panGestureRecognizer.addTarget(
self,
action: "panGestureCaptured:"
)
}
func panGestureCaptured(gesture: UIGestureRecognizer) {
let touchPoint = gesture.locationInView(self.webView)
print("touchPoint: \(touchPoint)")
print("panGestureCaptured scrollView offset \(self.webView!.scrollView.contentOffset.y)")
if self.webView!.scrollView.contentOffset.y == 0 {
if self.webView!.scrollView.panGestureRecognizer.state == UIGestureRecognizerState.Changed {
if touchPoint.y < self.webView!.frame.height * 0.3 {
self.spinnerTopLayoutConstraint?.constant = touchPoint.y
} else {
self.spinnerTopLayoutConstraint?.constant = self.webView!.frame.height * 0.3
}
} else if self.webView!.scrollView.panGestureRecognizer.state == UIGestureRecognizerState.Ended {
self.spinnerTopLayoutConstraint?.constant = UIApplication.sharedApplication().statusBarFrame.height + 20
}
}
}
WebView's scroll view already has a pan recognizer (webView.scrollView.panGestureRecognizer) so you could listen to this one (with addTarget) and disable bouncing on the scroll view. In this way you don't need to disable/enable the pan recognizer.

How to hide tab bar with animation in iOS?

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

Resources