1

After reading a few articles about custom UITabBarControllers, I am left more confused than before I even started doing the research in the first place.

My goal is to create a custom TabBar with 3 important properties:

  • No text, just icons
  • The active icon is marked by a circle filled with a color behind it, and therefore needs a different icon color

Here's what I am trying to achieve:

Example Tab Bar

I've been able to remove the text and center the icon by following another StackOverflow answer (Remove tab bar item text, show only image), although the solution seems like a hack to me.

How would I go about creating a circle behind the item and change the active item's color?

Also, would someone mind explaining the difference between the XCode inspector sections "Tab Bar Item" and "Bar Item", which appear directly under each other?

NikxDa
  • 4,137
  • 1
  • 26
  • 48

2 Answers2

1

The easiest and actually cleanest way to do it is to design your icons and import them as images to the .xcassets folder. Then you can just set the different icons for the different states for each of the viewControllers with:

ViewController.tabBarItem = UITabBarItem(title: "", image: yourImage.withRenderingMode(.alwaysOriginal), selectedImage: yourImage)

your selected image will be the one with the circle and the image will be without. It is way easier than manipulating the images in xcode and it is also less expensive since the compiler only has to render the images and doesn't have to manipulate them.

About the other question UIBarItem is

An abstract superclass for items that can be added to a bar that appears at the bottom of the screen.

UITabBarItem is a subclass of UIBarItem to provide extra funtionality.

Galo Torres Sevilla
  • 1,540
  • 2
  • 8
  • 15
  • Thanks! The other answer allowed more flexibility in terms of animations, but I really appreciate the explanation of the TabBarItem vs BarItem :) – NikxDa Jan 14 '19 at 10:49
1

The first step is simple: leaving the title property of the UITabbarItem empty should hide the label.

Your second step can actually be broken down into two steps: changing the color of the icon and adding a circle behind it.

The first step here is simple again: you can set a different icon to use for the currently selected ViewController (I use Storyboards, this process is pretty straightforward). What you'd do is add a white version of the icon to be shown when that menu option is selected.

The final step is displaying the circle. To do this, we'll need the following information:

  • Which item is currently selected?
  • What is the position of the icon on the screen?

The first of these two is pretty easy to find out, but the second poses a problem: the icons in a UITabBar aren't spaced around the screen equally, so we can't just divide the width of the tabbar by the amount of items in it, and then take half of that to find the center of the icons. Instead, we will subclass UITabBarController.

Note: the tabBar property of a UITabBarController does have a .selectionIndicatorImage property. You can assign an image to this and it will be shown behind your icon. However, you can't easily control the placement of this image, and that is why we still resort to subclassing UITabBarController.

 

class CircledTabBarController: UITabBarController {

    var circle: UIView?

    override func viewDidLoad() {
        super.viewDidLoad()
        let numberOfItems = CGFloat(tabBar.items!.count)
        let tabBarItemSize = CGSize(width: (tabBar.frame.width / numberOfItems) - 20, height: tabBar.frame.height)
        circle = UIView(frame: CGRect(x: 0, y: 0, width: tabBarItemSize.height, height: tabBarItemSize.height))
        circle?.backgroundColor = .darkGray
        circle?.layer.cornerRadius = circle!.frame.width/2
        circle?.alpha = 0
        tabBar.addSubview(circle!)
        tabBar.sendSubview(toBack: circle!)
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        let index = -(tabBar.items?.index(of: tabBar.selectedItem!)?.distance(to: 0))!
        let frame = frameForTabAtIndex(index: index)
        circle?.center.x = frame.origin.x + frame.width/2
        circle?.alpha = 1
    }

    override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
        let index = -(tabBar.items?.index(of: item)?.distance(to: 0))!
        let frame = frameForTabAtIndex(index: index)
        self.circle?.center.x = frame.origin.x + frame.width/2
    }

    func frameForTabAtIndex(index: Int) -> CGRect {
        var frames = tabBar.subviews.compactMap { (view:UIView) -> CGRect? in
            if let view = view as? UIControl {
                for item in view.subviews {
                    if let image = item as? UIImageView {
                        return image.superview!.convert(image.frame, to: tabBar)
                    }
                }
                return view.frame
            }
            return nil
        }
        frames.sort { $0.origin.x < $1.origin.x }
        if frames.count > index {
            return frames[index]
        }
        return frames.last ?? CGRect.zero
    }

}

Now use this subclass of UITabBarController instead of the base class.

So why this approach over simply changing the icon to a circled one? Because you can do many different things with this. I wrote an article about animating the UITabBarController in a similar manner, and if you like, you can easily use above implementation to add animation to yours too.

NikxDa
  • 4,137
  • 1
  • 26
  • 48
JillevdW
  • 1,087
  • 1
  • 10
  • 21
  • Great answer, and I like the approach you're taking. One question though: I am currently using the `tintColor` of the TabBar, and this applies to all items. When I have the circle, I need to change the tint of the selected item, but `tintColor` is a `UITabBar` property, not a `UITabBarItem` property. How would I do this? Would I really have to use a different icon? – NikxDa Jan 14 '19 at 10:40
  • As far as I'm aware, the image you add to the tabbar is shown in gray when it is deselected, and is only shown in the supplied `tintColor` when the item is selected. So in your case you set the `tintColor` property to `.white`. – JillevdW Jan 14 '19 at 10:45
  • 1
    Makes sense, I got something mixed up there. Thanks for your help! – NikxDa Jan 14 '19 at 10:49