15

Similar to what the Spotify or Apple Music app does when a song is playing, it places a custom view on top of the UITabBar: enter image description here

Solutions I've tried:

  1. UITabBarController in a ViewController with a max-sized Container View, and the custom view on top of the Container View49pt above the Bottom Layout Guide: enter image description here Problem: Any content in ViewControllers embedded in the UITabBarController constrained to the bottom don't show because they're hidden behind the custom layout. I've tried overriding size forChildContentContainer in UITabBarController, tried updating the bottom layout guide, Nothing. I need to resize the frame of container view of the UITabBarController.

  2. Tried #1 again, but tried solving the problem of content hiding behind it by increasing the size of UITabBar, and then using ImageInset on every TabBarItem to bring it down, and adding my custom view on top of the UITabBar. Hasn't worked really well. There are going to be times when I want to hide my custom view.

  3. UITabBarController as root, with each children being a ViewController with a Container View + my custom view: enter image description here But now I have multiple instances of my custom view floating around. If I want to change a label on it, have to change it to all views. Or hide, etc.

  4. Override the UITabBar property of UITabBarController and return my custom UITabBar (inflated it with a xib) that has a UITabBar + my custom view. Problem: Probably the most frustrating attempt of all. If you override that property with an instance of class MyCustomTabBar : UITabBar {}, no tab shows up! And yes, I set the delegate of myCustomTabBar to self.

Leaning towards #3, but looking for a better solution.

Community
  • 1
  • 1
azizj
  • 3,428
  • 2
  • 25
  • 33

7 Answers7

14

This is actually very easy if you subclass UITabBarController and add your view programmatically. Using this technique automatically supports rotation and size changes of the tab bar, regardless of which version you are on.

class CustomTabBarController: UITabBarController {
  override func viewDidLoad() {
    super.viewDidLoad()

    //...do some of your custom setup work
    // add a container view above the tabBar
    let containerView = UIView()
    containerView.backgroundColor = .red
    view.addSubview(containerView)
    containerView.translatesAutoresizingMaskIntoConstraints = false
    containerView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
    containerView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true

    // anchor your view right above the tabBar
    containerView.bottomAnchor.constraint(equalTo: tabBar.topAnchor).isActive = true

    containerView.heightAnchor.constraint(equalToConstant: 50).isActive = true
  }
}
noobular
  • 3,257
  • 2
  • 24
  • 19
  • 4
    This does not inset the view controllers inside the tab bar controller, which was the whole point – Jake Sep 27 '18 at 17:24
  • 1
    To inset the view controllers you can do: additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: 42, right: 0) in a base class for all the view controllers – WingJammer Oct 12 '20 at 20:38
  • additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: 42, right: 0) this does not work on iOS 14 though. – WingJammer Nov 03 '20 at 22:28
9

I got it!

enter image description here

In essence, I increased the size of the original UITabBar to accomodate a custom view (and to shrink the frame of the viewcontrollers above), and then adds a duplicate UITabBar + custom view right on top of it.

Here's the meat of what I had to do. I uploaded a functioning example of it and can be found in this repo:

class TabBarViewController: UITabBarController {

    var currentlyPlaying: CurrentlyPlayingView!
    static let maxHeight = 100
    static let minHeight = 49
    static var tabbarHeight = maxHeight

    override func viewDidLoad() {
        super.viewDidLoad()

        currentlyPlaying = CurrentlyPlayingView(copyFrom: tabBar)
        currentlyPlaying.tabBar.delegate = self

        view.addSubview(currentlyPlaying)
        tabBar.isHidden = true
    }
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        currentlyPlaying.tabBar.items = tabBar.items
        currentlyPlaying.tabBar.selectedItem = tabBar.selectedItem
    }
    func hideCurrentlyPlaying() {
        TabBarViewController.tabbarHeight = TabBarViewController.minHeight
        UIView.animate(withDuration: 0.5, animations: {
            self.currentlyPlaying.hideCustomView()
            self.updateSelectedViewControllerLayout()
        })
    }
    func updateSelectedViewControllerLayout() {
        tabBar.sizeToFit()
        tabBar.sizeToFit()
        currentlyPlaying.sizeToFit()
        view.setNeedsLayout()
        view.layoutIfNeeded()
        viewControllers?[self.selectedIndex].view.setNeedsLayout()
        viewControllers?[self.selectedIndex].view.layoutIfNeeded()
    }
}

extension UITabBar {

    open override func sizeThatFits(_ size: CGSize) -> CGSize {
        var sizeThatFits = super.sizeThatFits(size)
        sizeThatFits.height = CGFloat(TabBarViewController.tabbarHeight)
        return sizeThatFits
    }
}
azizj
  • 3,428
  • 2
  • 25
  • 33
9

Since iOS 11 this became a little easier. When you add your view, you can do the following:

viewControllers?.forEach {
   $0.additionalSafeAreaInsets = UIEdgeInsets(
      top: 0, 
      left: 0,
      bottom: yourView.height,
      right: 0
   )
}
Tiago Almeida
  • 14,081
  • 3
  • 67
  • 82
  • 2
    Your answer is really underappreciated imho. I would totally +2 it if I could :) – Jan Nash Oct 09 '20 at 14:33
  • This really is the best answer here. Use this function to set the safeAreaInsets for all viewControllers and then use `keyWindow = UIApplication.shared.windows.first(where: { $0.isKeyWindow })` to get the top most view of all views and add yourView `keyWindow.addSubview(yourView)`. Worked like a charm for me. – JustWorkAlready Jan 15 '21 at 23:13
  • 1
    You are a lifesaver! – FledgeXu Oct 06 '21 at 01:36
2

Your idea to put it in a wrapper viewcontroller is good, but it will only cause overhead (more viewcontrollers to load in memory), and issues when you want to change the code later on. If you want the bar to always show on your UITabBarController, then you should add it there.

You should subclass UITabBarController and load the custom bar from a nib. There you will have access to the tabbar (so you can place your bar correctly above it), and you will only load it in once (which solves your problem that you will face having a different bar on each tab).

As for your views not reacting to the size of the custom bar, I don't know how you can do that, but my best suggestion is to use a public variable and notifications that you listen to in your individual tabs.
You can then use that to change the bottom constraint.

vrwim
  • 13,020
  • 13
  • 63
  • 118
  • I can load the custom view from a nib in UITabBarController, but then how would I display it? Somehow have the UITabBarController show it? (that'd be ideal), or have each of the children view display it like shown in #3? – azizj Feb 22 '17 at 14:56
  • Subclass the UITabBarController and load it there from a `nib`. Then you can add it to the `view` and constrain it in code to the top of the tabbar. You could constrain it with a fixed 49 pixels (the height of the tabbar) or use the location of the tabbar. – vrwim Feb 22 '17 at 15:32
  • Ah right. But then the problem of children viewcontrollers extending below the custom view... I could put a container view between the TabBarController and its children to limit the size, similar to #3, but show the custom view in the TabBarController. – azizj Feb 22 '17 at 16:10
  • I'm afraid you will just have to constrain your tabs to stop at a certain height. Another option could be to just create your own viewcontroller that has a tabbar, your custom view and a container view for the content. You could then handle your own tab switching. This will require a lot more work sadly, but that is the way to go if you don't want to make each tab stop before your view. – vrwim Feb 22 '17 at 16:25
  • Posted a solution incorporating your answer. :) Let me know what you think – azizj Feb 23 '17 at 19:36
1

Besides playing with UITabBar or container vc, you could also consider adding the view in the App Delegate to the main window like in following post:

View floating above all ViewControllers

Since your view is all around along with the Tab bar, it is totally ok to make it in the App Delegate.

You can always access the Floating view from App Delegate Singleton by making it a property of the App Delegate. It is easy then to control its visibility in anywhere of your code.

Changing constant of the Constraints between the Floating view and super view window can adjust the position of the view, thus handsomely respond to orientation changes.

Another(similar) approach is to make the floating view another window like the uid button.

1

Unless I've misunderstood, you could create a custom view from your UITabBarController class. You can then insert it above and constrain it to the tabBar object, which is the tabBar associated with the controller.

So from your UITabBarController class, create your custom view

class CustomTabBarController: UITabBarController {
    var customView: UIView = {
            let bar = UIView()
            bar.backgroundColor = .white
            bar.translatesAutoresizingMaskIntoConstraints = false
            return bar
        }()

In viewDidLoad() add your custom view to the UITabBarController's view object and place it above the tabBar object

override func viewDidLoad() {
        super.viewDidLoad()
        
        ...

        self.view.insertSubview(customView, aboveSubview: tabBar)

Then after your custom view is added as a subView, add constraints so it's positioned correctly. This should also be done in viewDidLoad() but only after your view is inserted.

self.view.addConstraints([
            NSLayoutConstraint(item: customView, attribute: .leading, relatedBy: .equal, toItem: tabBar, attribute: .leading, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: customView, attribute: .trailing, relatedBy: .equal, toItem: tabBar, attribute: .trailing, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: customView, attribute: .top, relatedBy: .equal, toItem: tabBar, attribute: .top, multiplier: 1, constant: -50),
            NSLayoutConstraint(item: customView, attribute: .bottom, relatedBy: .equal, toItem: tabBar, attribute: .top, multiplier: 1, constant: 0)
            ])

There's a bunch of creative ways you can setup constraints to do what you want, but the constraints above should attach a view above your tabBar with a height of 50.

UKnow
  • 21
  • 1
  • 3
  • I have found only one issue with that code. When you try to push new viewController to the navigationController with hidesBottomBarWhenPushed set to true you will notice that the view will be missing when u will return to the previous viewController. – ShadeToD Dec 28 '20 at 10:07
  • Interesting, I've never used or modified this property so I've never run into this issue. It's possible that when views are hidden all constraints associated with the view get messed with (this should be looked into, I'm just guessing). If that's the case, then the constraints I mentioned above would have to be reapplied every time the tabbar reappears (moves from hidden to visible). – UKnow Dec 31 '20 at 19:35
0
  1. Make the view's frame with the height of tab bar and brings it to top, 2. set tabBar hidden is true.
Ning
  • 148
  • 1
  • 10