I am working on UIPanGestureRecognizer and to me it is working. but I have some problem here as I am new to iOS and just shifted from Android to iOS.
First take a look at what I want to do:
What I want: I have a UITableView and I want to perform swiping on the Cells. I just want to drag them from left to right side and move/Delete that cell. Pretty same like it is done in android.
But I just want to move the item only in one direction. And that is "LEFT TO RIGHT". But not from right to left. Now here take a look at what I have done so far
What I have Done:
#objc func handlePan(recognizer: UIPanGestureRecognizer) {
// 1
if recognizer.state == .began {
// when the gesture begins, record the current center location
originalCenter = center
print("Center",originalCenter)
}
// 2
if recognizer.state == .changed {
let translation = recognizer.translation(in: self)
center = CGPoint(x: originalCenter.x+translation.x, y: originalCenter.y)
// has the user dragged the item far enough to initiate a delete/complete?
deleteOnDragRelease = frame.origin.x < -frame.size.width / 2.0
completeOnDragRelease = frame.origin.x > frame.size.width / 2.0
// print ("FrameX = ",frame.origin.x , " , ","Width = ",frame.size.width / 2.0 , "Total = ",frame.origin.x < -frame.size.width / 2.0 )
//print ("DelOnDrag = ",deleteOnDragRelease , " , ","CompOnDrag = ",completeOnDragRelease)
}
// 3
if recognizer.state == .ended {
// the frame this cell had before user dragged it
let originalFrame = CGRect(x: 0, y: frame.origin.y,
width: bounds.size.width, height: bounds.size.height)
if deleteOnDragRelease {
if delegate != nil && clickedItem != nil {
// notify the delegate that this item should be deleted
delegate!.toDoItemDeleted(clickedItem: clickedItem!)
}
} else if completeOnDragRelease {
UIView.animate(withDuration: 8.2, animations: {self.frame = originalFrame})
} else {
UIView.animate(withDuration: 8.2, animations: {self.frame = originalFrame})
}
}
}
I know I can make a check on ".changed" , and calculate if the X value is going towards 0 or lesser then 0. But point is for some time it will move item from right to left.
Question: Is there any way I can get the x value of point of contact? or just some how I can get user want to swipe right to left and just stop user from doing that?? Please share your knowledge
your same code just one changes in your UIGestureRecognizer method replace with this code and your problem solve. only left to right side swap work on your tableview cell . any query regrading this just drop comment below.
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer {
let translation = panGestureRecognizer.translation(in: superview!)
if translation.x >= 0 {
return true
}
return false
}
return false
}
Good Luck.
Keep coding.
You can do this using below extensions
extension UIPanGestureRecognizer {
enum GestureDirection {
case Up
case Down
case Left
case Right
}
func verticalDirection(target: UIView) -> GestureDirection {
return self.velocity(in: target).y > 0 ? .Down : .Up
}
func horizontalDirection(target: UIView) -> GestureDirection {
return self.velocity(in: target).x > 0 ? .Right : .Left
}
}
And you can get direction like below
gestureRecognizer.horizontalDirection(target: self)
I am rather new to iOS and swift 3 programming. At the moment I am working on a little learning project. I have the following problem.
On my screen I have 4 UIViews and 4 UIImageViews. The app allows to “drag and drop” an image onto one of the UIViews. This works pretty well so far. I use Pan Gesture Recognisers for the dragging of UIImages. But for some reason one of the UIViews seems to be in front of the image, meaning that I can drop the image to this area, but I cannot see it - it is layered under the UIView. The other three behave properly, the UIImageView is layered on top of the UIView. In the viewDidLoad() function of the view controller I set the background color of the UIViews to grey. If I don’t do that, the image will be displayed even on the fourth UIView.
I have checked for differences between the four UIView objects but cannot find any. I also tried to recreate the UIView by copying it from one of the the other three.
So my question is: Is there any property which defines that the UIView is layered on top or can be sent to back so that other objects are layered on top of it? Or is there anything else I am missing? Any hints would be very appreciated.
I was not quite sure which parts of the code are relevant, so I posted some of it (but still guess that this is rather a property of the UIView…)
This is where I set the background of the UIViews
override func viewDidLoad() {
super.viewDidLoad()
lbViewTitle.text = "\(viewTitle) \(level) - \(levelPage)"
if (level==0) {
print("invalid level => EXIT")
} else {
loadData(level: level)
originalPositionLetter1 = letter1.center
originalPositionLetter2 = letter2.center
originalPositionLetter3 = letter3.center
originalPositionLetter4 = letter4.center
dropArea1.layer.backgroundColor = UIColor.lightGray.cgColor
dropArea2.layer.backgroundColor = UIColor.lightGray.cgColor
dropArea3.layer.backgroundColor = UIColor.lightGray.cgColor
dropArea4.layer.backgroundColor = UIColor.lightGray.cgColor
}
}
This is the code that moves the image:
#IBAction func hadlePan1(_ sender: UIPanGestureRecognizer) {
moveLetter(sender: sender, dropArea: dropArea4, movedImage: letter1, originalPosition: originalPositionLetter1)
}
func moveLetter(sender: UIPanGestureRecognizer, dropArea : UIView, movedImage : UIImageView, originalPosition : CGPoint) {
let translation = sender.translation(in: self.view)
if let view = sender.view {
view.center = CGPoint(x:view.center.x + translation.x,
y:view.center.y + translation.y)
if sender.state == UIGestureRecognizerState.ended {
let here = sender.location(in: self.view)
if (dropArea.frame.contains(here)) {
movedImage.center = dropArea.center
} else {
movedImage.center = originalPosition
}
}
}
sender.setTranslation(CGPoint.zero, in: self.view)
}
There are two methods that may be useful to you: UIView.sendSubview(toBack:) and UIView.bringSubview(toFront:).
What I think you should do is to call bringSubview(toFront:) when you start dragging the image:
func moveLetter(sender: UIPanGestureRecognizer, dropArea : UIView, movedImage : UIImageView, originalPosition : CGPoint) {
self.view.bringSubview(toFront: movedImage) // <--- add this line!
let translation = sender.translation(in: self.view)
if let view = sender.view {
view.center = CGPoint(x:view.center.x + translation.x,
y:view.center.y + translation.y)
if sender.state == UIGestureRecognizerState.ended {
let here = sender.location(in: self.view)
if (dropArea.frame.contains(here)) {
movedImage.center = dropArea.center
} else {
movedImage.center = originalPosition
}
}
}
sender.setTranslation(CGPoint.zero, in: self.view)
}
Im trying to make a small game. And there is some problem with the animation. Im new to Swift. So lets take a look. I create a UIImageView picture and want to do animation of this picture appear in a different places on a screen. I believe that the algorithm will look like this:
Infinite loop{
1-GetRandomPlace
2-change opacity from 0 to 1 and back(with smooth transition)
}
Looks simple, but I can't understand how to do it correctly in Xcode.
Here is my test code but it looks useless
Thank you for help and sorry if there was already this question, I can't find it.
class ViewController: UIViewController {
#IBOutlet weak var BackgroundMainMenu:UIImageView!
#IBOutlet weak var AnimationinMenu: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
// MoveBackgroundObject(AnimationinMenu)
// AnimationBackgroundDots(AnimationinMenu, delay: 0.0)
// self.AnimationinMenu.alpha = 0
//
// UIImageView.animateWithDuration(3.0,
// delay: 0.0,
// options: UIViewAnimationOptions([.Repeat, .CurveEaseInOut]),
// animations: {
// self.MoveBackgroundObject(self.AnimationinMenu)
// self.AnimationinMenu.alpha = 1
// self.AnimationinMenu.alpha = 0
// },
// completion: nil)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewWillAppear(animated: Bool) {
AnimationBackgroundDots(AnimationinMenu, delay: 0.0)
}
func ChangeOpacityto1(element: UIImageView){
element.alpha = 0
UIView.animateWithDuration(3.0) {
element.alpha = 1
}
}
func ChangeOpacityto0(element: UIImageView){
element.alpha = 1
UIView.animateWithDuration(3.0){
element.alpha = 0
}
}
func AnimationBackgroundDots(element: UIImageView, delay: Double){
element.alpha = 0
var z = 0
while (z<4){
MoveBackgroundObject(AnimationinMenu)
UIImageView.animateWithDuration(3.0,
animations: {
element.alpha = 0
element.alpha = 1
element.alpha = 0
},
completion: nil)
z++
}
}
func MoveBackgroundObject(element: UIImageView) {
// Find the button's width and height
let elementWidth = element.frame.width
let elementHeight = element.frame.height
// Find the width and height of the enclosing view
let viewWidth = BackgroundMainMenu.superview!.bounds.width
let viewHeight = BackgroundMainMenu.superview!.bounds.height
// Compute width and height of the area to contain the button's center
let xwidth = viewWidth - elementWidth
let yheight = viewHeight - elementHeight
// Generate a random x and y offset
let xoffset = CGFloat(arc4random_uniform(UInt32(xwidth)))
let yoffset = CGFloat(arc4random_uniform(UInt32(yheight)))
// Offset the button's center by the random offsets.
element.center.x = xoffset + elementWidth / 2
element.center.y = yoffset + elementHeight / 2
}
}
The problem is in AnimationBackgroundDots.
You are immediately creating 4 animations on the same view but only one can run at a time. What you need to do is wait until one animation is finished (fade in or fade out) before starting a new one.
Also, the animations closure is for setting the state you want your view to animate to. It looks at how your view is at the start, runs animations, then looks at the view again and figures out how to animate between the two. In your case, the alpha of the UIImageView starts at 0, then when animations runs, the alpha ends up being 0 so nothing would animate. You can't create all the steps an animation should take that way.
Want you need to do it move your view and start the fade in animation. The completion closure of fading in should start the fade out animation. The completion closure of the fading out should then start the process all over again. It could look something like this.
func AnimationBackgroundDots(element: UIImageView, times: Int) {
guard times > 0 else {
return
}
MoveBackgroundObject(element)
element.alpha = 1
// Fade in
UIView.animateWithDuration(3.0, animations: {
element.alpha = 1
}, completion: { finished in
// Fade Out
UIView.animateWithDuration(3.0, animations: {
element.alpha = 0
}, completion: { finished in
// Start over again
self.AnimationBackgroundDots(element, times: times-1)
})
})
}
You called also look at using keyframe animations but this case is simple enough that theres no benefit using them.
Also as a side note. The function naming convention in swift is to start with a lowercase letter, so AnimationBackgroundDots should be animationBackgroundDots.
I would like to receive updates from the uipageviewcontroller during the page scrolling process. I want to know the transitionProgress in %. (This value should update when the user move the finger in order to get to another page). I'm interested in the animation progress from one page to another, not the progress through the total number of pages.
What I have found so far:
There is a class called UICollectionViewTransitionLayout that have the property corresponding to what I am looking for, "transitionProgress". Probably uipageviewcontroller implement this method somehow?
I can call the following method on the uipagecontroller but I only get 0 as result!
CGFloat percentComplete = [self.pageViewController.transitionCoordinator percentComplete];
in SWIFT to copy paste ;) works perfect for me
extension UIPageViewController: UIScrollViewDelegate {
public override func viewDidLoad() {
super.viewDidLoad()
for subview in view.subviews {
if let scrollView = subview as? UIScrollView {
scrollView.delegate = self
}
}
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
let point = scrollView.contentOffset
var percentComplete: CGFloat
percentComplete = abs(point.x - view.frame.size.width)/view.frame.size.width
print("percentComplete: ",percentComplete)
}
}
At last I found out a solution, even if it is probably not the best way to do it:
I first add an observer on the scrollview like this:
// Get Notified at update of scrollview progress
NSArray *views = self.pageViewController.view.subviews;
UIScrollView* sW = [views objectAtIndex:0];
[sW addObserver:self forKeyPath:#"contentOffset" options:NSKeyValueObservingOptionNew context:NULL];
And when the observer is called:
NSArray *views = self.pageViewController.view.subviews;
UIScrollView* sW = [views objectAtIndex:0];
CGPoint point = sW.contentOffset;
float percentComplete;
//iPhone 5
if([ [ UIScreen mainScreen ] bounds ].size.height == 568){
percentComplete = fabs(point.x - 568)/568;
} else{
//iphone 4
percentComplete = fabs(point.x - 480)/480;
}
NSLog(#"percentComplete: %f", percentComplete);
I'm very happy that I found this :-)
Since I thought that the functionality of scrolling would stay forever, but that the internal implementation may change to something other than a scroll view, I found the solution below (I haven't tested this very much, but still)
NSUInteger offset = 0;
UIViewController * firstVisibleViewController;
while([(firstVisibleViewController = [self viewControllerForPage:offset]).view superview] == nil) {
++offset;
}
CGRect rect = [[firstVisibleViewController.view superview] convertRect:firstVisibleViewController.view.frame fromView:self.view];
CGFloat absolutePosition = rect.origin.x / self.view.frame.size.width;
absolutePosition += (CGFloat)offset;
(self is the UIPageViewController here, and [-viewControllerForPage:] is a method that returns the view controller at the given page)
If absolutePosition is 0.0f, then the first view controller is shown, if it's equal to 1.0f, the second one is shown, etc... This can be called repeatedly in a CADisplayLink along with the delegate methods and/or UIPanGestureRecognizer to effectively know the status of the current progress of the UIPageViewController.
EDIT: Made it work for any number of view controllers
Use this -
for (UIView *v in self.pageViewController.view.subviews) {
if ([v isKindOfClass:[UIScrollView class]]) {
((UIScrollView *)v).delegate = self;
}
}
to implement this protocol : -(void)scrollViewDidScroll:(UIScrollView *)scrollView
and then use #xhist's code (modified) in this way
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGPoint point = scrollView.contentOffset;
float percentComplete;
percentComplete = fabs(point.x - self.view.frame.size.width)/self.view.frame.size.width;
NSLog(#"percentComplete: %f", percentComplete);
}
Based on Appgix solution, I'm adding this directly on my 'UIPageViewController' subclass. (Since I only need it on this one)
For Swift 3:
class MYPageViewControllerSubclass: UIPageViewController, UIScrollViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
for subView in view.subviews {
if subView is UIScrollView {
(subView as! UIScrollView).delegate = self
}
}
}
// MARK: - Scroll View Delegate
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
let point = scrollView.contentOffset
var percentComplete: CGFloat
percentComplete = fabs(point.x - view.frame.size.width)/view.frame.size.width
NSLog("percentComplete: %f", percentComplete)
}
// OTHER CODE GOES HERE...
}
While Appgix' solution seemed to work at first, I noticed that when the user pans in a UIPageViewController, lifts the finger shortly and then immediately starts dragging again while the "snap-back" animation is NOT YET finished and then lifts his finger again (which will again "snap-back"), the scrollViewDidScroll method is only called when the page view controller finished the animation.
For the progress calculation this means the second pan produces continuous values like 0.11, 0.13, 0.16 but when the scroll view snaps back the next progress value will be 1.0 which causes my other scroll view to be out of sync.
To fight this I'm now listening to the scroll view's contentOffset key, which is still updated continuously in this situation.
KVO approach for Swift 4
var myContext = 0
override func viewDidLoad() {
for view in self.view.subviews {
if view is UIScrollView {
view.addObserver(self, forKeyPath: "contentOffset", options: .new, context: &introPagingViewControllerContext)
}
}
}
// MARK: KVO
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?)
{
guard let change = change else { return }
if context != &myContext {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}
if keyPath == "contentOffset" {
if let contentOffset = change[NSKeyValueChangeKey.newKey] as? CGPoint {
let screenWidth = UIScreen.main.bounds.width
let percent = abs((contentOffset.x - screenWidth) / screenWidth)
print(percent)
}
}
}
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.