How to hide tab bar with animation in iOS? - 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.

Related

How to update the frameOfPresentedViewInContainerView when using custom modal presentation?

I'm using a custom UIPresentationController to present a view modally. After presenting the view, the first textfield in the presented view becomes the first responder and the keyboard shows up. To ensure that the view is still visible, I move it up. However, when I do this the frameOfPresentedViewInContainerView is not matching the actual frame of the view anymore. Because of this, when I tap on the view it's being dismissed, because there's a tapGestureRecogziner on the backgroundView which is on top of the presentingView. How to notify the presentingController that the frame/position of the presentedView has changed?
In the UIPresentationController:
override var frameOfPresentedViewInContainerView: CGRect {
var frame = CGRect.zero
let safeAreaBottom = self.presentingViewController.view.safeAreaInsets.bottom
guard let height = presentedView?.frame.height else { return frame }
if let containerBounds = containerView?.bounds {
frame = CGRect(x: 0,
y: containerBounds.height - height - safeAreaBottom,
width: containerBounds.width,
height: height + safeAreaBottom)
}
return frame
}
override func presentationTransitionWillBegin() {
if let containerView = self.containerView, let coordinator = presentingViewController.transitionCoordinator {
containerView.addSubview(self.dimmedBackgroundView)
self.dimmedBackgroundView.backgroundColor = .black
self.dimmedBackgroundView.frame = containerView.bounds
self.dimmedBackgroundView.alpha = 0
coordinator.animate(alongsideTransition: { _ in
self.dimmedBackgroundView.alpha = 0.5
}, completion: nil)
}
}
Presenting the view modally:
let overlayVC = CreateEventViewController()
overlayVC.transitioningDelegate = self.transitioningDelegate
overlayVC.modalPresentationStyle = .custom
self.present(overlayVC, animated: true, completion: nil)
Animation when keyboard appears (in the presented view):
#objc func animateWithKeyboard(notification: NSNotification) {
let userInfo = notification.userInfo!
guard let keyboardHeight = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height,
let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double,
let curve = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt else {
return
}
// bottomContraint is the constraint that pins content to the bottom of the superview.
let moveUp = (notification.name == UIResponder.keyboardWillShowNotification)
bottomConstraint.constant = moveUp ? (keyboardHeight) : originalBottomValue
let options = UIView.AnimationOptions(rawValue: curve << 16)
UIView.animate(withDuration: duration, delay: 0,
options: options,
animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
From the Apple documentation:
UIKit calls this method multiple times during the course of a
presentation, so your implementation should return the same frame
rectangle each time. Do not use this method to make changes to your
view hierarchy or perform other one-time tasks.
AFAIK, if you specify frame through this variable, it's advised not to change it throughout the course of presentation. If you plan to play around with the frames, don't specify this variable and handle all the changes manually in your animator

Interactive View Controller Transition - Weird behavior (stuck in the transition)

I'm trying to implement a custom pan gesture to interactively transition to a new view controller. The way it works is that I have a button (labeled "Template Editor", see below) on which you can start a pan to move the current view controller to the right, revealing the new view controller next to it (I've recorded my problem, see below).
Everything is working but there is a bug that I don't understand at all:
Sometimes, when I just swipe over the button (triggering a pan gesture) then lift my finger again (touch down -> fast, short swipe to the right -> touch up) the interactive transition glitches out. It starts to very slowly complete the transition and afterwards, I cannot dismiss the presented view controller, nor can I present anything on that presented view controller.
I have no idea why. Here's my code:
First, the UIViewControllerAnimatedTransitioning class. It's implemented using UIViewPropertyAnimator and just adds the animation using transform:
class MovingTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
enum Direction {
case left, right
}
// MARK: - Properties
// ========== PROPERTIES ==========
private var animator: UIViewImplicitlyAnimating?
var duration = 0.6
var presenting = true
var shouldAnimateInteractively: Bool = false
public var direction: Direction = .left
private var movingMultiplicator: CGFloat {
return direction == .left ? -1 : 1
}
// ====================
// MARK: - Initializers
// ========== INITIALIZERS ==========
// ====================
// MARK: - Overrides
// ========== OVERRIDES ==========
// ====================
// MARK: - Functions
// ========== FUNCTIONS ==========
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let animator = interruptibleAnimator(using: transitionContext)
animator.startAnimation()
}
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
// If the animator already exists, return it (important, see documentation!)
if let animator = self.animator {
return animator
}
// Otherwise, create the animator
let containerView = transitionContext.containerView
let fromView = transitionContext.view(forKey: .from)!
let toView = transitionContext.view(forKey: .to)!
if presenting {
toView.frame = containerView.frame
toView.transform = CGAffineTransform(translationX: movingMultiplicator * toView.frame.width, y: 0)
} else {
toView.frame = containerView.frame
toView.transform = CGAffineTransform(translationX: -movingMultiplicator * toView.frame.width, y: 0)
}
containerView.addSubview(toView)
let animator = UIViewPropertyAnimator(duration: duration, dampingRatio: 0.9, animations: nil)
animator.addAnimations {
if self.presenting {
toView.transform = .identity
fromView.transform = CGAffineTransform(translationX: -self.movingMultiplicator * toView.frame.width, y: 0)
} else {
toView.transform = .identity
fromView.transform = CGAffineTransform(translationX: self.movingMultiplicator * toView.frame.width, y: 0)
}
}
animator.addCompletion { (position) in
// Important to set frame above (device rotation will otherwise mess things up)
toView.transform = .identity
fromView.transform = .identity
if !transitionContext.transitionWasCancelled {
self.shouldAnimateInteractively = false
}
self.animator = nil
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
self.animator = animator
return animator
}
// ====================
}
Here's the part that adds the interactivity. It's a method that's being called by a UIPanGestureRecognizer I added to the button.
public lazy var transitionAnimator: MovingTransitionAnimator = MovingTransitionAnimator()
public lazy var interactionController = UIPercentDrivenInteractiveTransition()
...
#objc private func handlePan(pan: UIPanGestureRecognizer) {
let translation = pan.translation(in: utilityView)
var progress = (translation.x / utilityView.frame.width)
progress = CGFloat(fminf(fmaxf(Float(progress), 0.0), 1.0))
switch pan.state {
case .began:
// This is a flag that helps me distinguish between when a user taps on the button and when he starts a pan
transitionAnimator.shouldAnimateInteractively = true
// Just a dummy view controller that's dismissing as soon as its been presented (the problem occurs with every view controller I use here)
let vc = UIViewController()
vc.view.backgroundColor = .red
vc.transitioningDelegate = self
present(vc, animated: true, completion: {
self.transitionAnimator.shouldAnimateInteractively = false
vc.dismiss(animated: true, completion: nil)
})
case .changed:
interactionController.update(progress)
case .cancelled:
interactionController.cancel()
case .ended:
if progress > 0.55 || pan.velocity(in: utilityView).x > 600
interactionController.completionSpeed = 0.8
interactionController.finish()
} else {
interactionController.completionSpeed = 0.8
interactionController.cancel()
}
default:
break
}
}
I also implemented all the necessary delegate methods:
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transitionAnimator.presenting = true
return transitionAnimator
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transitionAnimator.presenting = false
return transitionAnimator
}
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
guard let animator = animator as? MovingTransitionAnimator, animator.shouldAnimateInteractively else { return nil }
return interactionController
}
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
guard let animator = animator as? MovingTransitionAnimator, animator.shouldAnimateInteractively else { return nil }
return interactionController
}
That's it. There's no more logic behind it (I think; if you need more information, please tell me), but it still has this bug. Here's a recording of the bug. You can't really see my touch but all I'm doing is touching down -> fast, shortly swiping to the right -> touching up. And after this really slow transition has finished, I can't dismiss the red view controller. It's stuck there:
Here's what's even stranger:
Neither interactionController.finish() nor interactionController.cancel() is being called when this occurs (at least not from within my handlePan(_:)method).
I checked the view hierarchy in Xcode after this bug occurred and I got this:
First, it's seemingly stuck in the transition (everything is still inside UITransitionView).
Second, on the left hand side you see the views of the first view controller(the one I start the transition from). However, on the image there only is the red view controller visible, the one that was about to be presented.
Do you have any idea what's going on? I've been trying to figure this out for the past 3 hours but I can't get get it to work properly. I'd appreciate any help
Thank you!
EDIT
Okay, I found a way to reproduce it 100% of the time. I also created an isolated project demonstrating the problem (it's a little differently structured because I tried many things but the result is still exactly the same)
Here's the project: https://github.com/d3mueller/InteractiveTransitionDemo2
How to reproduce the problem:
Swipe from right to left and then quickly from left to right. This will trigger the bug.
Also, a similar bug will appear, when you swipe from right to left very fast multiple times. Then it will actually run the transition and finish it correctly (but it shouldn't even start because moving from right to left keeps the progress at 0.0)
You might try setting:
/// Set this to NO in order to start an interruptible transition non
/// interactively. By default this is YES, which is consistent with the behavior
/// before 10.0.
#property (nonatomic) BOOL wantsInteractiveStart NS_AVAILABLE_IOS(10_0);
to NO on your interactionController
Good luck and curious to hear if you figure it out.

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 present UIViewController on top of current view controller with smaller height

I would like to present a UIViewController on top of the current view controller and set it's height to ~80% of the screen size. I've got the first part:
let additionalVC = ChartsViewController(currentSelection)
additionalVC = .overCurrentContext
present(additionalVC, animated: true)
I tried setting the self.view.frame inside my ChartsVC in viewDidLoad and couple of different things but it is always presented in the full screen mode.
That's what I want to achieve:
blueVC - currentVC
redVC - ChartsVC - VC on top of the current VC with ~80% of the original height
btw I'm doing everything programmatically, no xib and UIStoryboard.
There's a number of ways to achieve this.
You could use a 3rd party framework (http://transitiontreasury.com/) or the way I would do this.
Present the newVC where a transition = model over current context
ensure the newVC.views background color is clear
add another view where origin.y is the distance between the top and the desired gap. This is the view where all your objects will sit on.
If you need a coding example let me know, but its a pretty simple solution and looking at your code your 80% there.
Thomas
Implement a custom UIPresentationController. To use a custom view size, you only need to override a single property.
This code will simply inset the presented view controller by 50x100 pts:
class MyPresentationController: UIPresentationController {
// Inset by 50 x 100
override var frameOfPresentedViewInContainerView: CGRect {
return self.presentingViewController.view.bounds.insetBy(dx: 50, dy: 100)
}
}
To darken the presenting view controller, override presentationTransitionWillBegin() and dismissalTransitionWillBegin() to insert a shading view and animate it into view:
class MyPresentationController: UIPresentationController {
override var frameOfPresentedViewInContainerView: CGRect {
return self.presentingViewController.view.bounds.insetBy(dx: 50, dy: 100)
}
let shadeView = UIView()
override func presentationTransitionWillBegin() {
self.shadeView.backgroundColor = UIColor.black
self.shadeView.alpha = 0
// Insert the shade view above the presenting view controller
self.shadeView.frame = self.presentingViewController.view.frame
self.containerView?.insertSubview(shadeView,
aboveSubview: self.presentingViewController.view)
// Animate it into view
self.presentingViewController.transitionCoordinator?.animate(alongsideTransition: { (context) in
self.shadeView.alpha = 0.3
}, completion: nil)
}
override func dismissalTransitionWillBegin() {
self.presentingViewController.transitionCoordinator?.animate(alongsideTransition: { (context) in
self.shadeView.alpha = 0.0
}, completion: nil)
}
}
To use your custom presentation controller, set the modalPresentationStyle and transitioningDelegate:
class MyViewController : UIViewController, UIViewControllerTransitioningDelegate {
//
// Your code
//
func presentCharts() {
let additionalVC = ChartsViewController(currentSelection)
additionalVC.modalPresentationStyle = .custom
additionalVC.transitioningDelegate = self
self.present(additionalVC, animated: true)
}
//
// UIViewControllerTransitioningDelegate protocol
//
func presentationController(forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController) -> UIPresentationController? {
return MyPresentationController(presentedViewController: presented,
presenting: presenting)
}
}
In IOS 13 and Xcode 11, you can present ViewController with modalPresentationStyle = .automatic
Take Two ViewController.First view controller have a button and the button action name is clicked.The target is to clicking the button we want to add secondVC as a child of first view controller and show secondVC 80% of the first view controller.again click button we remove secondVC from first view controller. below is the code for click button action.
#IBAction func clicked(_ sender: UIButton) {
if !isshown{
isshown = true
self.addChildViewController(vc)
self.view.addSubview(vc.view)
vc.didMove(toParentViewController: self)
let height = view.frame.height
let width = view.frame.width
UIView.animate(withDuration: 0.3, delay: 0, options: UIViewAnimationOptions.curveEaseIn, animations: {
self.vc.view.frame = CGRect(x: 0, y: 100 , width: width, height: height - 100)
}, completion: { (result) in
// do what you want to do
})
}else{
isshown = false
UIView.animate(withDuration: 0.3,
delay: 0,
options: UIViewAnimationOptions.curveEaseIn,
animations: { () -> Void in
var frame = self.vc.view.frame
frame.origin.y = UIScreen.main.bounds.maxY
self.vc.view.frame = frame
}, completion: { (finished) -> Void in
self.vc.view.removeFromSuperview()
self.vc.removeFromParentViewController()
})
}
}
here vc is a reference of secondVC.
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Second") as! secondVC
change below piece of code to get whatever percentage you want.
self.vc.view.frame = CGRect(x: 0, y: 100 , width: width, height: height - 100)

UIView.transitionWithView breaks the loader's layout

I am trying to animate the root-view-controller-change in my app. After I swap the view controllers, I load the data necessary for the 2nd controller right away. While the data is loading, I show a loader(MBProgressHUD). This is my function for swapping the view controllers:
class ViewUtils {
class func animateRootViewController(duration: NSTimeInterval, changeToViewController: UIViewController) {
let window = UIApplication.sharedApplication().delegate?.window?
if window == nil {
return
}
UIView.transitionWithView(window!,
duration: duration,
options: UIViewAnimationOptions.TransitionFlipFromLeft | UIViewAnimationOptions.AllowAnimatedContent,
animations: {
window!.rootViewController = changeToViewController
},
completion: nil
)
}
}
All good with this but one thing - it totally breaks the loader. I am attaching an imagine of what's happening:
This is the 2nd view controller while rotating. Once the rotation is complete, the loader appears just fine, both the spinner and the text tween to the correct position in the rounded rectangle.
I really don't understand why this happens, would somebody explain it to me, please? Is there a way to prevent it?
The code of the 2nd view controller where I show the loader:
override func viewDidLoad() {
super.viewDidLoad()
hud = HUD(containingView: view)
hud.show()
createBackground()
}
And my hud class:
class HUD {
private var hudBG: UIView!
private var view: UIView!
private(set) var isShown = false
init(containingView: UIView) {
view = containingView
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func show() {
if !isShown {
if(hudBG == nil) {
hudBG = UIView(frame: CGRectMake(0, 0, view.bounds.width, view.bounds.height))
hudBG.backgroundColor = UIColor(white: 0, alpha: 0.4)
}
view.addSubview(hudBG)
let hud = MBProgressHUD.showHUDAddedTo(view, animated: true)
hud.mode = MBProgressHUDModeIndeterminate
hud.labelText = "Cargando"
hudBG.alpha = 0
UIView.animateWithDuration(0.3, animations: { () -> Void in
self.hudBG.alpha = 1
})
isShown = true
}
}
func hide() {
if isShown {
UIView.animateWithDuration(0.3, animations: {
() -> Void in
self.hudBG.alpha = 0
}, completion: {
(b) -> Void in
self.hudBG.removeFromSuperview()
})
MBProgressHUD.hideHUDForView(view, animated: true)
isShown = false
}
}
}
Thanks a lot for any ideas!
You are adding the hud to a view that is not properly initialized yet.
If you are loading the view controller from a xib or storyboard, the view and it's subviews have the size as they were loaded from interface.
You have to add the hud after the views have been resized to their final size.
If you move
hud = HUD(containingView: view)
hud.show()
to viewDidLayoutSubviews, it should work fine.
I noticed a similar problem when moving an app from iOS 7 to iOS 8. During animations, especially when scaling was involved, the view positions got distorted.
I am pretty sure it's a bug. The simplest workaround is to animate only screenshots or view snapshots, not actual views - it's more work and you can't have views animating when the main animation is in progress but in general it's a more stable solution.

Resources