Related
I have a tableView.tableHeaderView which is a UITableViewHeaderFooterView.
I would like this header to initially not be displayed when the tableViewController is first presented. When the user scrolls down from the top of the tableView, I would like to present the header.
Then when the user slightly scrolls down, I want the header to snap to a hidden position. The behaviors is exactly like the archived chats header of the WhatsApp page where all your chat's are listed.
Is there any way to achieve this without a complex set of scrollview delegate calls?
I thought on previous versions of swift/xcode, the tableView.tableHeaderView kind of snapped up and down but I notice it's not doing that anymore.
I think the only solution might be overriding the scrollViewDidScroll. This is what I've done but when the tableView.headerView reappears, it does so over the first cell of the tableView. Not sure how to make it appear in the correct position.
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if let myHeaderView = self.tableView.tableHeaderView as? MyTableViewHeaderFooterView {
let height = (self.navigationController?.navigationBar.frame.size.height)! + UIApplication.shared.statusBarFrame.size.height + self.searchController.searchBar.frame.size.height
if scrollView.contentOffset.y < -height {
UIView.animate(withDuration: 0.1) {
myHeaderView.frame.size.height = 44
}
}
}
}
This is what I've settled on which seems to work pretty well in all cases:
var minimumTableViewInset = CGFloat(88) // this can be adjusted to 32 when the tableView is in landscape. need observer for rotation to set that.
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UINib(nibName: "MyTableViewHeaderFooterView", bundle: nil), forHeaderFooterViewReuseIdentifier: "MyTableViewHeaderFooterView")
let listsHeader = tableView.dequeueReusableHeaderFooterView(withIdentifier: "MyTableViewHeaderFooterView") as! MyTableViewHeaderFooterView
listsHeader.alpha = 0
listsHeader.frame.size.height = 0
self.tableView.tableHeaderView = listsHeader
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if let headerView = self.tableView.tableHeaderView as? MyTableViewHeaderFooterView {
let height = (self.navigationController?.navigationBar.frame.size.height)! + UIApplication.shared.statusBarFrame.size.height + self.searchController.searchBar.frame.size.height
if scrollView.contentOffset.y < -height {
if headerView.frame.size.height != 44 {
tableView.beginUpdates()
headerView.frame.size.height = 44
tableView.endUpdates()
UIView.animate(withDuration: 0.5) {
self.tableView.tableHeaderView?.alpha = 1
}
}
}
}
}
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if let headerView = self.tableView.tableHeaderView as? MyTableViewHeaderFooterView {
if scrollView.contentOffset.y > -(minimumTableViewInset - CGFloat(10)) {
if headerView.frame.size.height != 0 {
tableView.beginUpdates()
headerView.frame.size.height = 0
tableView.endUpdates()
UIView.animate(withDuration: 0.2) {
self.tableView.tableHeaderView?.alpha = 0
}
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: UITableViewScrollPosition.bottom, animated: true)
}
}
}
}
override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if let headerView = self.tableView.tableHeaderView as? MyTableViewHeaderFooterView {
if scrollView.contentOffset.y > -(minimumTableViewInset - CGFloat(10)) {
if headerView.frame.size.height != 0 {
tableView.beginUpdates()
headerView.frame.size.height = 0
tableView.endUpdates()
UIView.animate(withDuration: 0.2) {
self.tableView.tableHeaderView?.alpha = 0
}
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: UITableViewScrollPosition.bottom, animated: true)
}
}
}
}
I want to scroll to an position close to one of my cells in my UITableView.
I do this with this code:
myTableView.scrollToRow(at: index,at: .top, animated: true)
and after the animation is completed i do:
myTableView.setContentOffset(CGPoint(x: 0, y: myTableView.contentOffset.y + myCloseToCellOffset), animated: true)
The problem however is that the animation is split in two animations and therefor doesn't run smoothly. Is there anyway to make this to one animation? Or any other way to get the result i want?
In my opinion this unpleasant pause between the two animations is unpleasant only when the direction of the two scrolls is the same. When it is different (first up, then down) it looks fine to me. So I am proposing the following (maybe there is a simpler solution, but this is what I thought of):
Calculate the offset achieved by myTableView.scrollToRow(at: index,at: .top, animated: true) by yourself.
If this offset change will be in the same direction as there will be on the addition of myCloseToCellOffset, just add myCloseToCellOffset to it and call myTableView.setContentOffset(CGPoint(x: 0, y: myTableView.contentOffset.y + (initalOffset + myCloseToCellOffset), animated: true) directly
If this offset change will differ in direction than the offset change achieved by myCloseToCellOffset then make two calls. Firstly myTableView.setContentOffset(CGPoint(x: 0, y: myTableView.contentOffset.y + initalOffset, animated: true) and then `myTableView.setContentOffset(CGPoint(x: 0, y: myTableView.contentOffset.y + myCloseToCellOffset, animated: true)
I don't know what myCloseToCellOffset is, so I suppose it could be positive or negative or zero.
Here is some example implementation, I hope I don't have mistakes and I hope I am verbose enough:
enum TableViewChangeDirections {
case upUp
case downDown
case upDown
case downUp
case unimportant
}
extension Double {
/// Rounds the double to decimal places value
func rounded(toPlaces places:Int) -> Double {
let divisor = pow(10.0, Double(places))
return (self * divisor).rounded() / divisor
}
}
Then in your View Controller that holds the table view
let numberOfCells = 20
let cellToMakeVisibleIndex = 5
let heightOfCell : CGFloat = 44
let myCloseToCellOffset : CGFloat = 100
var firstDesiredYOffset: CGFloat!
var secondDesiredYOffset: CGFloat!
This will find the offset of scrollToRow
func tableViewOffset(forCellToGoTo cellToGoTo: CGFloat) -> CGFloat {
let desiredOffset = heightOfCell * cellToGoTo
var maxContentOffset = CGFloat(numberOfCells)*heightOfCell - tableView.frame.size.height
maxContentOffset = (maxContentOffset >= 0) ? maxContentOffset : 0
return (desiredOffset > maxContentOffset) ? maxContentOffset : desiredOffset
}
This will execute the second offset change if the directions are opposite:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (tableView.isUserInteractionEnabled == false && self.secondDesiredYOffset != nil && self.firstDesiredYOffset != nil && Double(tableView.contentOffset.y).rounded(toPlaces: 2) == Double(self.firstDesiredYOffset).rounded(toPlaces: 2)) {
self.tableView.setContentOffset(CGPoint(x: 0, y: self.secondDesiredYOffset), animated: true)
self.secondDesiredYOffset = nil
self.tableView.isUserInteractionEnabled = true
}
}
This will return the directions of the two offset changes:
var changeDirections : TableViewChangeDirections {
if self.firstDesiredYOffset > self.tableView.contentOffset.y && myCloseToCellOffset >= 0 {
return .upUp
} else if self.firstDesiredYOffset > self.tableView.contentOffset.y && myCloseToCellOffset < 0 {
return .upDown
} else if self.firstDesiredYOffset < self.tableView.contentOffset.y && myCloseToCellOffset <= 0 {
return .downDown
} else if self.firstDesiredYOffset < self.tableView.contentOffset.y && myCloseToCellOffset > 0 {
return .downUp
} else {
return .unimportant
}
}
And finally the place where you make the general offset changes. I put it in a button action, but you put it where you need it to be:
#IBAction func didPressButton(_ sender: Any) {
self.firstDesiredYOffset = self.tableViewOffset(forCellToGoTo: CGFloat(cellToMakeVisibleIndex))
self.tableView.setContentOffset(CGPoint(x: 0, y: self.firstDesiredYOffset), animated: true)
switch (self.changeDirections) {
case .upUp, .downDown:
self.firstDesiredYOffset = self.firstDesiredYOffset + myCloseToCellOffset
self.tableView.setContentOffset(CGPoint(x: 0, y: self.firstDesiredYOffset), animated: true)
case .upDown, .downUp:
self.tableView.isUserInteractionEnabled = false
self.secondDesiredYOffset = self.firstDesiredYOffset + myCloseToCellOffset
self.tableView.setContentOffset(CGPoint(x: 0, y: self.firstDesiredYOffset), animated: true)
case .unimportant:
self.tableView.setContentOffset(CGPoint(x: 0, y: self.tableView.contentOffset.y + myCloseToCellOffset), animated: true)
}
}
I have UIScrollView inside which I have an ImageView. What I am trying to achieve is:
- zoom-in image to fit full height of the screen when user taps and zoom-out when user taps again,
- start automatically first animation which is scrolling to the right side of the image,
- after that, start second animation which is scrolling to the middle of the image,
- at last start again the animation scrolling to the right side of the image.
I would like to achieve the impression of image floating on the screen. User cannot scroll the image by himself.
I have already done the part with zooming, but the problem I have is with animations. I have tried something like this, where offset, middleOffset and endOffset are CGPoints:
override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self
setupGestureRecognizer()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
scrollView.panGestureRecognizer.isEnabled = false
scrollView.pinchGestureRecognizer?.isEnabled = false
}
fileprivate func updateMinZoomScaleForSize(size: CGSize) {
let widthScale = size.width / imageView.bounds.width
let heightScale = size.height / imageView.bounds.height
let minScale = min(widthScale, heightScale)
let maxHeightScale = view.frame.height / imageView.bounds.height
scrollView.maximumZoomScale = maxHeightScale
scrollView.minimumZoomScale = minScale
scrollView.zoomScale = minScale
}
fileprivate func updateConstraintsForSize(size: CGSize) {
let yOffset = max(0, (size.height - imageView.frame.height) / 2)
imageViewTopConstraint.constant = yOffset
imageViewBottomConstraint.constant = yOffset
let xOffset = max(0, (size.width - imageView.frame.width) / 2)
imageViewLeadingConstraint.constant = xOffset
imageViewTrailingConstraint.constant = xOffset
view.layoutIfNeeded()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
updateMinZoomScaleForSize(size: view.bounds.size)
}
func setupGestureRecognizer() {
let singleTap = UITapGestureRecognizer(target: self, action: #selector(self.handleSingleTap(recognizer:)))
singleTap.numberOfTapsRequired = 1
scrollView.addGestureRecognizer(singleTap)
}
func handleSingleTap(recognizer: UITapGestureRecognizer) {
if (scrollView.zoomScale > scrollView.minimumZoomScale) {
scrollView.setZoomScale(scrollView.minimumZoomScale, animated: true)
} else {
scrollView.setZoomScale(scrollView.maximumZoomScale, animated: true)
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
UIView.animate(withDuration: 1, delay: 0.3, options: [], animations: {
scrollView.setContentOffset(offset, animated: false)
}, completion: { _ in
UIView.animate(withDuration: 1, delay: 0.3, options: [], animations: {
scrollView.setContentOffset(middleOffset, animated: false)
}, completion: { _ in
UIView.animate(withDuration: 1, delay: 0.3, options: [], animations: {
scrollView.setContentOffset(endOffset, animated: false)
}, completion: nil)
})
})
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
updateConstraintsForSize(size: view.bounds.size)
}
Howewer it does not work. First animation loads after image is zoomed in, but then it stops (it does not crash). It seems like animation is not finished because I cannot zoom-out image when tapping again.
Ok, I found out that animation blocks cannot be inside of scrollViewDidScroll delegate method. I put them inside handleSingleTap function and then it works as it should.
I'm quite new to iOS development. Right now i'm trying to hide my tabbar when I scroll down and when scrolling up the tabbar should appear. I would like to have this animated in the same way like the navigation bar. For the navigation bar I simply clicked the option in the Attributes Inspector. I saw some examples for the toolbar, but I cant adopt it the tabbar.
self.tabBarController?.tabBar.hidden = true just hides my tabbar, but its not animated like the navigation controller.
This is code that i'm actually using in a production app.
It's in Swift and it also updates UITabBar.hidden var.
func scrollViewWillBeginDragging(scrollView: UIScrollView) {
if scrollView.panGestureRecognizer.translation(in: scrollView).y < 0{
changeTabBar(hidden: true, animated: true)
}
else{
changeTabBar(hidden: false, animated: true)
}
}
You can also use the other callback method:
func scrollViewDidScroll(scrollView: UIScrollView) {
...
}
but if you choose so, then you must handle multiple calls to the helper method that actually hides the tabBar.
And then you need to add this method that animates the hide/show of the tabBar.
func changeTabBar(hidden:Bool, animated: Bool){
var tabBar = self.tabBarController?.tabBar
if tabBar!.hidden == hidden{ return }
let frame = tabBar?.frame
let offset = (hidden ? (frame?.size.height)! : -(frame?.size.height)!)
let duration:NSTimeInterval = (animated ? 0.5 : 0.0)
tabBar?.hidden = false
if frame != nil
{
UIView.animateWithDuration(duration,
animations: {tabBar!.frame = CGRectOffset(frame!, 0, offset)},
completion: {
println($0)
if $0 {tabBar?.hidden = hidden}
})
}
}
Update Swift 4
func changeTabBar(hidden:Bool, animated: Bool){
guard let tabBar = self.tabBarController?.tabBar else { return; }
if tabBar.isHidden == hidden{ return }
let frame = tabBar.frame
let offset = hidden ? frame.size.height : -frame.size.height
let duration:TimeInterval = (animated ? 0.5 : 0.0)
tabBar.isHidden = false
UIView.animate(withDuration: duration, animations: {
tabBar.frame = frame.offsetBy(dx: 0, dy: offset)
}, completion: { (true) in
tabBar.isHidden = hidden
})
}
This answer is a slight modification to Ariel answer which adds animation while user scrolls.
extension ViewController:UIScrollViewDelegate{
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.panGestureRecognizer.translation(in: scrollView).y < 0{
//scrolling down
changeTabBar(hidden: true, animated: true)
}
else{
//scrolling up
changeTabBar(hidden: false, animated: true)
}
}
func changeTabBar(hidden:Bool, animated: Bool){
let tabBar = self.tabBarController?.tabBar
let offset = (hidden ? UIScreen.main.bounds.size.height : UIScreen.main.bounds.size.height - (tabBar?.frame.size.height)! )
if offset == tabBar?.frame.origin.y {return}
print("changing origin y position")
let duration:TimeInterval = (animated ? 0.5 : 0.0)
UIView.animate(withDuration: duration,
animations: {tabBar!.frame.origin.y = offset},
completion:nil)
}
}
Building on Ariel's answer, I have updated the code for Swift3. This worked great on my collection views.
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if scrollView.panGestureRecognizer.translation(in: scrollView).y < 0 {
changeTabBar(hidden: true, animated: true)
}else{
changeTabBar(hidden: false, animated: true)
}
}
func changeTabBar(hidden:Bool, animated: Bool){
let tabBar = self.tabBarController?.tabBar
if tabBar!.isHidden == hidden{ return }
let frame = tabBar?.frame
let offset = (hidden ? (frame?.size.height)! : -(frame?.size.height)!)
let duration:TimeInterval = (animated ? 0.5 : 0.0)
tabBar?.isHidden = false
if frame != nil
{
UIView.animate(withDuration: duration,
animations: {tabBar!.frame = frame!.offsetBy(dx: 0, dy: offset)},
completion: {
print($0)
if $0 {tabBar?.isHidden = hidden}
})
}
}
You can control UITabBar precisly by setting up your class as delegate for scrollView and implementing scrolling in scrollViewDidScroll: method.
Here is an example how I do it my application. You can probably easily modify that for your needs. Some helper function to get UITabBar included.
#define LIMIT(__VALUE__, __MIN__, __MAX__) MAX(__MIN__, MIN(__MAX__, __VALUE__))
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGFloat scrollOffset = scrollView.contentOffset.y;
CGFloat scrollDiff = scrollOffset - self.previousScrollViewYOffset;
CGFloat scrollHeight = scrollView.frame.size.height;
CGFloat scrollContentSizeHeight = scrollView.contentSize.height + scrollView.contentInset.bottom;
CGFloat scrollOffsetGlobal = scrollOffset + scrollView.contentInset.top;
[self updateUITabBarY:[self UITabBarView].frame.origin.y + scrollDiff];
self.previousScrollViewYOffset = scrollOffset;
}
- (UITabBar*) UITabBarView
{
for(UIView *view in self.tabBarController.view.subviews)
{
if([view isKindOfClass:[UITabBar class]])
{
return (UITabBar*) view;
}
}
return nil;
}
- (void) updateUITabBarY:(CGFloat) y
{
UITabBar* tabBar = [self UITabBarView];
if(tabBar)
{
CGRect frame = tabBar.frame;
frame.origin.y = LIMIT(y, [self UITabBarMiny], [self UITabBarMaxY]);
tabBar.frame = frame;
}
}
- (CGFloat) UITabBarMiny
{
return [UIScreen mainScreen].bounds.size.height - [self UITabBarView].frame.size.height - [[UIApplication sharedApplication] statusBarFrame].size.height + 20.0f;
}
- (CGFloat) UITabBarMaxY
{
return [UIScreen mainScreen].bounds.size.height;
}
Ariels answer works, but has some values that seem off. When you compare the y-value of the scrollView scrollView.panGestureRecognizer.translation(in: scrollView).y, "0" has the side effect, that the tabBar shows or hides when you stop scrolling. It calls the method one more time with a "0" value. I tried it with didEndDragging, didScroll and willBeginDragging with similar effects. And that feels very counter intuitive or buggy.
I used +/- 0.1 when comparing the y-value and got the desired effect, that it just shows and hides when you are really scrolling up or down.
Another thing that isn't mentioned is that the offset that you set with tabBar.frame = frame.offsetBy(dx: 0, dy: offset) will be reset when the app moves to the background. You scroll down, the tabBar disappears, you change the app, open it up again, the tabBar is still hidden but the frame is back to the old location. So when the function is called again, the tabBar moves up even more and you have a gap of the size of the tabBar.frame.
To get rid of this I compared the current frame location and animated the alpha value. I couldn't get the usual coming back up animation to work, maybe somebody will try, can't be that hard. But its okay this way, as it doesn't happen that often.
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let yValue = scrollView.panGestureRecognizer.translation(in: scrollView).y
if yValue < -0.1 {
//hide tabBar
changeTabBar(hidden: true, animated: true)
} else if yValue > 0.1 {
//show tabBar
changeTabBar(hidden: false, animated: true)
}
}
func changeTabBar(hidden:Bool, animated: Bool) {
guard let tabBar = self.tabBarController?.tabBar else {
return
}
if tabBar.isHidden == hidden{
return
}
let frame = tabBar.frame
let frameMinY = frame.minY //lower end of tabBar
let offset = hidden ? frame.size.height : -frame.size.height
let viewHeight = self.view.frame.height
//hidden but moved back up after moving app to background
if frameMinY < viewHeight && tabBar.isHidden {
tabBar.alpha = 0
tabBar.isHidden = false
UIView.animate(withDuration: 0.5) {
tabBar.alpha = 1
}
return
}
let duration:TimeInterval = (animated ? 0.5 : 0.0)
tabBar.isHidden = false
UIView.animate(withDuration: duration, animations: {
tabBar.frame = frame.offsetBy(dx: 0, dy: offset)
}, completion: { (true) in
tabBar.isHidden = hidden
})
}
According to #Ariel Hernández Amador answer for black screen after hiding Tabbar just use this line of code in your ViewDidLoad(). Working Superbly...I have posted this here as I am unable to comment over there.
viewDidLoad()
{
if #available(iOS 11.0, *) {
self.myScroll.contentInsetAdjustmentBehavior = .never
}
}
Here myScroll is the Scrollview I am using in my VC. Just replace it with your VC.
I am trying to mimic the UINavigationController's new hidesBarsOnTap with a tab bar. I have seen many answers to this that either point to setting the hidesBottomBarWhenPushed on a viewController which only hides it entirely and not when tapped.
#IBAction func tapped(sender: AnyObject) {
// what goes here to show/hide the tabBar ???
}
thanks in advance
EDIT: as per the suggestion below I tried
self.tabBarController?.tabBar.hidden = true
which does indeed hide the tabBar (toggles true/false on tap), but without animation. I will ask that as a separate question though.
After much hunting and trying out various methods to gracefully hide/show the UITabBar using Swift I was able to take this great solution by danh and convert it to Swift:
func setTabBarVisible(visible: Bool, animated: Bool) {
//* This cannot be called before viewDidLayoutSubviews(), because the frame is not set before this time
// bail if the current state matches the desired state
if (tabBarIsVisible() == visible) { return }
// get a frame calculation ready
let frame = self.tabBarController?.tabBar.frame
let height = frame?.size.height
let offsetY = (visible ? -height! : height)
// zero duration means no animation
let duration: TimeInterval = (animated ? 0.3 : 0.0)
// animate the tabBar
if frame != nil {
UIView.animate(withDuration: duration) {
self.tabBarController?.tabBar.frame = frame!.offsetBy(dx: 0, dy: offsetY!)
return
}
}
}
func tabBarIsVisible() -> Bool {
return (self.tabBarController?.tabBar.frame.origin.y)! < self.view.frame.maxY
}
// Call the function from tap gesture recognizer added to your view (or button)
#IBAction func tapped(_ sender: Any?) {
setTabBarVisible(visible: !tabBarIsVisible(), animated: true)
}
Loved Michael Campsall's answer. Here's the same code as extension, if somebody is interested:
Swift 2.3
extension UITabBarController {
func setTabBarVisible(visible:Bool, animated:Bool) {
// bail if the current state matches the desired state
if (tabBarIsVisible() == visible) { return }
// get a frame calculation ready
let frame = self.tabBar.frame
let height = frame.size.height
let offsetY = (visible ? -height : height)
// animate the tabBar
UIView.animateWithDuration(animated ? 0.3 : 0.0) {
self.tabBar.frame = CGRectOffset(frame, 0, offsetY)
self.view.frame = CGRectMake(0, 0, self.view.frame.width, self.view.frame.height + offsetY)
self.view.setNeedsDisplay()
self.view.layoutIfNeeded()
}
}
func tabBarIsVisible() ->Bool {
return self.tabBar.frame.origin.y < CGRectGetMaxY(self.view.frame)
}
}
Swift 3
extension UIViewController {
func setTabBarVisible(visible: Bool, animated: Bool) {
//* This cannot be called before viewDidLayoutSubviews(), because the frame is not set before this time
// bail if the current state matches the desired state
if (isTabBarVisible == visible) { return }
// get a frame calculation ready
let frame = self.tabBarController?.tabBar.frame
let height = frame?.size.height
let offsetY = (visible ? -height! : height)
// zero duration means no animation
let duration: TimeInterval = (animated ? 0.3 : 0.0)
// animate the tabBar
if frame != nil {
UIView.animate(withDuration: duration) {
self.tabBarController?.tabBar.frame = frame!.offsetBy(dx: 0, dy: offsetY!)
return
}
}
}
var isTabBarVisible: Bool {
return (self.tabBarController?.tabBar.frame.origin.y ?? 0) < self.view.frame.maxY
}
}
I had to adapt the accepted answer to this question a bit. It was hiding the bar but my view wasn't sizing itself appropriately so I was left with a space at the bottom.
The following code successfully animates the hiding of the tab bar while resizing the view to avoid that issue.
Updated for Swift 3 (now with less ugly code)
func setTabBarVisible(visible: Bool, animated: Bool) {
guard let frame = self.tabBarController?.tabBar.frame else { return }
let height = frame.size.height
let offsetY = (visible ? -height : height)
let duration: TimeInterval = (animated ? 0.3 : 0.0)
UIView.animate(withDuration: duration,
delay: 0.0,
options: UIViewAnimationOptions.curveEaseIn,
animations: { [weak self] () -> Void in
guard let weakSelf = self else { return }
weakSelf.tabBarController?.tabBar.frame = frame.offsetBy(dx: 0, dy: offsetY)
weakSelf.view.frame = CGRect(x: 0, y: 0, width: weakSelf.view.frame.width, height: weakSelf.view.frame.height + offsetY)
weakSelf.view.setNeedsDisplay()
weakSelf.view.layoutIfNeeded()
})
}
func handleTap(recognizer: UITapGestureRecognizer) {
setTabBarVisible(visible: !tabBarIsVisible(), animated: true)
}
func tabBarIsVisible() -> Bool {
guard let tabBar = tabBarController?.tabBar else { return false }
return tabBar.frame.origin.y < UIScreen.main.bounds.height
}
Older Swift 2 Version
func setTabBarVisible(visible: Bool, animated: Bool) {
// hide tab bar
let frame = self.tabBarController?.tabBar.frame
let height = frame?.size.height
var offsetY = (visible ? -height! : height)
println ("offsetY = \(offsetY)")
// zero duration means no animation
let duration:NSTimeInterval = (animated ? 0.3 : 0.0)
// animate tabBar
if frame != nil {
UIView.animateWithDuration(duration) {
self.tabBarController?.tabBar.frame = CGRectOffset(frame!, 0, offsetY!)
self.view.frame = CGRectMake(0, 0, self.view.frame.width, self.view.frame.height + offsetY!)
self.view.setNeedsDisplay()
self.view.layoutIfNeeded()
return
}
}
}
#IBAction func handleTap(recognizer: UITapGestureRecognizer) {
setTabBarVisible(!tabBarIsVisible(), animated: true)
}
func tabBarIsVisible() -> Bool {
return self.tabBarController?.tabBar.frame.origin.y < UIScreen.mainScreen().bounds.height
}
You can just add this line to ViewDidLoad() in swift :
self.tabBarController?.tabBar.hidden = true
I use tabBar.hidden = YES in ObjC to hide the tab bar in certain cases. I have not tried wiring it up to a tap event, though.
Code is okay but when you use presentViewController, tabBarIsVisible() is not working. To keep UITabBarController always hidden use just this part:
extension UITabBarController {
func setTabBarVisible(visible:Bool, animated:Bool) {
let frame = self.tabBar.frame
let height = frame.size.height
let offsetY = (visible ? -height : height)
UIView.animateWithDuration(animated ? 0.3 : 0.0) {
self.tabBar.frame = CGRectOffset(frame, 0, offsetY)
self.view.frame = CGRectMake(0, 0, self.view.frame.width, self.view.frame.height + offsetY)
self.view.setNeedsDisplay()
self.view.layoutIfNeeded()
}
}
}
Swift 3 version:
func setTabBarVisible(visible:Bool, animated:Bool) {
//* This cannot be called before viewDidLayoutSubviews(), because the frame is not set before this time
// bail if the current state matches the desired state
if (tabBarIsVisible() == visible) { return }
// get a frame calculation ready
let frame = self.tabBarController?.tabBar.frame
let height = frame?.size.height
let offsetY = (visible ? -height! : height)
// zero duration means no animation
let duration:TimeInterval = (animated ? 0.3 : 0.0)
// animate the tabBar
if frame != nil {
UIView.animate(withDuration: duration) {
self.tabBarController?.tabBar.frame = (self.tabBarController?.tabBar.frame.offsetBy(dx: 0, dy: offsetY!))!
return
}
}
}
func tabBarIsVisible() ->Bool {
return (self.tabBarController?.tabBar.frame.origin.y)! < self.view.frame.midY
}
Swift 5
To hide
override func viewWillAppear(_ animated: Bool) {
self.tabBarController?.tabBar.isHidden = true
}
To show again
override func viewDidDisappear(_ animated: Bool) {
self.tabBarController?.tabBar.isHidden = false
}
For Swift 4, and animating + hiding by placing tabBar outside the view:
if let tabBar = tabBarController?.tabBar,
let y = tabBar.frame.origin.y + tabBar.frame.height {
UIView.animate(withDuration: 0.2) {
tabBar.frame = CGRect(origin: CGPoint(x: tabBar.frame.origin.x, y: y), size: tabBar.frame.size)
}
}
To make the animations work with self.tabBarController?.tabBar.hidden = true just do this:
UIView.animateWithDuration(0.2, animations: {
self.tabBarController?.tabBar.hidden = true
})
Other than the other solution this will also work nicely with autolayout.