40

Is there a way to find the frame of a particular UITabBarItem in a UITabBar?

Specifically, I want to create an animation of an image "falling" into one of the tabs, similar to e.g. deleting an email in the Mail, or buying a track in the iTunes app. So I need the target coordinates for the animation.

As far as I can tell, there's no public API to get the coordinates, but would love to be wrong about that. Short of that, I'll have to guesstimate the coordinates using the index of the given tab relative to the tab bar frame.

Daniel Dickison
  • 21,832
  • 13
  • 69
  • 89
  • Hey, Have you found the solution for this in swift..If yes then can you please help me .. – Rahul Apr 18 '17 at 12:20

11 Answers11

26

Imre's implementation is missing a couple of imho important details.

  1. The UITabBarButton views are not necessarily in order. For example, if you have more than 5 tabs on iPhone and rearranged tabs, the views might be out of order.
  2. If you use more than 5 tabs the out of bounds index only means that the tab is behind the "more" tab. In this case there is no reason to fail with an assert, just use the frame of the last tab.

So I changed his code a little bit and I came up with this:

+ (CGRect)frameForTabInTabBar:(UITabBar*)tabBar withIndex:(NSUInteger)index
{
    NSMutableArray *tabBarItems = [NSMutableArray arrayWithCapacity:[tabBar.items count]];
    for (UIView *view in tabBar.subviews) {
        if ([view isKindOfClass:NSClassFromString(@"UITabBarButton")] && [view respondsToSelector:@selector(frame)]) {
            // check for the selector -frame to prevent crashes in the very unlikely case that in the future
            // objects thar don't implement -frame can be subViews of an UIView
            [tabBarItems addObject:view];
        }
    }
    if ([tabBarItems count] == 0) {
        // no tabBarItems means either no UITabBarButtons were in the subView, or none responded to -frame
        // return CGRectZero to indicate that we couldn't figure out the frame
        return CGRectZero;
    }

    // sort by origin.x of the frame because the items are not necessarily in the correct order
    [tabBarItems sortUsingComparator:^NSComparisonResult(UIView *view1, UIView *view2) {
        if (view1.frame.origin.x < view2.frame.origin.x) {
            return NSOrderedAscending;
        }
        if (view1.frame.origin.x > view2.frame.origin.x) {
            return NSOrderedDescending;
        }
        NSAssert(NO, @"%@ and %@ share the same origin.x. This should never happen and indicates a substantial change in the framework that renders this method useless.", view1, view2);
        return NSOrderedSame;
    }];

    CGRect frame = CGRectZero;
    if (index < [tabBarItems count]) {
        // viewController is in a regular tab
        UIView *tabView = tabBarItems[index];
        if ([tabView respondsToSelector:@selector(frame)]) {
            frame = tabView.frame;
        }
    }
    else {
        // our target viewController is inside the "more" tab
        UIView *tabView = [tabBarItems lastObject];
        if ([tabView respondsToSelector:@selector(frame)]) {
            frame = tabView.frame;
        }
    }
    return frame;
}
Matthias Bauch
  • 89,811
  • 20
  • 225
  • 247
  • 15
    Given that UITabBarButton is a private iOS class and Apple's AppStore review process is getting more thorough, I think it prudent to not have any references to that class at all -- even via NSClassFromString(). But it is a UIControl subclass and all UIControls implement -(CGRect)frame. So you can eliminate a bunch of conditions with this: `if ([view isKindOfClass:[UIControl class]]) [tabBarItems addObject:view];` – Greg Combs Sep 16 '13 at 22:32
  • This seems to return incorrect results after rotating the device. I rotate from portrait to landscape but it returns portrait values. When I rotate from landscape to portrait it returns landscape values. I'm running this code in `viewDidLayoutSubviews`. Any thoughts? – BFeher Oct 06 '14 at 08:04
  • In iOS 10, the assert is getting hit. It appears all the subviews have an origin of zero, zero when that happens, so the views just haven't been laid out yet. I'll probably just wait to call this method until it's finished laying everything out. – Aaron Ash Sep 21 '16 at 22:06
21

Another possible but sloppy solution would be the following:

guard let view = self.tabBarVC?.tabBar.items?[0].valueForKey("view") as? UIView else 
{
    return
}
let frame = view.frame
redead
  • 360
  • 3
  • 16
18

The subviews associated with the tab bar items in a UITabBar are of class UITabBarButton. By logging the subviews of a UITabBar with two tabs:

for (UIView* view in self.tabBar.subviews)
{
    NSLog(view.description);
}

you get:

<_UITabBarBackgroundView: 0x6a91e00; frame = (0 0; 320 49); opaque = NO; autoresize = W; userInteractionEnabled = NO; layer = <CALayer: 0x6a91e90>> - (null)
<UITabBarButton: 0x6a8d900; frame = (2 1; 156 48); opaque = NO; layer = <CALayer: 0x6a8db10>>
<UITabBarButton: 0x6a91b70; frame = (162 1; 156 48); opaque = NO; layer = <CALayer: 0x6a8db40>>

Based on this the solution is kind of trivial. The method I wrote for trying this out is as follows:

+ (CGRect)frameForTabInTabBar:(UITabBar*)tabBar withIndex:(NSUInteger)index
{
    NSUInteger currentTabIndex = 0;

    for (UIView* subView in tabBar.subviews)
    {
        if ([subView isKindOfClass:NSClassFromString(@"UITabBarButton")])
        {
            if (currentTabIndex == index)
                return subView.frame;
            else
                currentTabIndex++;
        }
    }

    NSAssert(NO, @"Index is out of bounds");
    return CGRectNull;
}

It should be noted that the structure (subviews) of UITabBar and the class UITabBarButton itself are not part of the public API, so in theory it can change in any new iOS version without prior notification. Nevertheless it is unlikely that they would change such detail, and it works fine with iOS 5-6 and prior versions.

Imre Kelényi
  • 22,113
  • 5
  • 35
  • 45
  • +1 for ipad, but This solution doesnt work for iPhone/ipod. This gives wrong coordinates. – iLearner Apr 29 '13 at 14:36
  • Works on iPhone with iOS 9.2 pretty well. I have used ```UIControl```(@Greg Combs suggestion) as a kind of subview class in comparison. – rafalkitta Mar 16 '16 at 15:04
14

In Swift 4.2:

private func frameForTab(atIndex index: Int) -> CGRect {
    var frames = view.subviews.compactMap { (view:UIView) -> CGRect? in
        if let view = view as? UIControl {
            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
}
Graham Perks
  • 23,007
  • 8
  • 61
  • 83
buildsucceeded
  • 4,203
  • 4
  • 34
  • 72
  • This works great on an iPhone but seems to work incorrectly on the iPad. On the iPad, the items (or at least - images) are placed in the center while this returns equally wide frames. – KlimczakM Oct 10 '16 at 10:36
  • @KlimczakM This should return the frame of the actual item— if you know the image size you could create CGRects inside the `flatMap` call, of the given size, centered on `view.center`? – buildsucceeded Oct 18 '16 at 12:44
13

By Extending UITabBar

extension UITabBar {

    func getFrameForTabAt(index: Int) -> CGRect? {
        var frames = self.subviews.compactMap { return $0 is UIControl ? $0.frame : nil }
        frames.sort { $0.origin.x < $1.origin.x }
        return frames[safe: index]
    }

}

extension Collection {

    subscript (safe index: Index) -> Element? {
        return indices.contains(index) ? self[index] : nil
    }

}
Lausbert
  • 1,471
  • 2
  • 17
  • 23
6

Tab bar buttons are the only sub views that have user interaction enabled. Check for this instead of UITabBarButton to avoid violating hidden API's.

for (UIView* view in self.tabBar.subviews)
    {
        if( [view isUserInteractionEnabled] )
        {
            [myMutableArray addObject:view];
        }
}

Once in the array, sort it based on the origin x, and you will have the tab bar buttons in their true order.

TigerCoding
  • 8,710
  • 10
  • 47
  • 72
3

I'll add what worked for me in my simple UITabBarController scenario, everything is legit but it has an assumption that items are spaced equally. Under iOS7 it returns an instance of an UITabBarButton, but if you'll be using it as UIView* you really don't care what it is and you aren't stating the class explicitly. The frame of the returned view is the frame you're looking for:

-(UIView*)viewForTabBarItemAtIndex:(NSInteger)index {

    CGRect tabBarRect = self.tabBarController.tabBar.frame;
    NSInteger buttonCount = self.tabBarController.tabBar.items.count;
    CGFloat containingWidth = tabBarRect.size.width/buttonCount;
    CGFloat originX = containingWidth * index ;
    CGRect containingRect = CGRectMake( originX, 0, containingWidth, self.tabBarController.tabBar.frame.size.height );
    CGPoint center = CGPointMake( CGRectGetMidX(containingRect), CGRectGetMidY(containingRect));
    return [ self.tabBarController.tabBar hitTest:center withEvent:nil ];
}

What it does is calculate the rect in the UITabBar where the button resides, finds the center of this rect and digs out the view at that point via hitTest:withEvent.

maksa
  • 374
  • 5
  • 16
3

Swift + iOS 11

private func frameForTabAtIndex(index: Int) -> CGRect {
    guard let tabBarSubviews = tabBarController?.tabBar.subviews else {
        return CGRect.zero
    }
    var allItems = [UIView]()
    for tabBarItem in tabBarSubviews {
        if tabBarItem.isKind(of: NSClassFromString("UITabBarButton")!) {
            allItems.append(tabBarItem)
        }
    }
    let item = allItems[index]
    return item.superview!.convert(item.frame, to: view)
}
Phil
  • 1,077
  • 1
  • 11
  • 18
2

Swift 5:

Just put this in your View Controller that handles your tabBar.

guard let tab1frame = self.tabBar.items![0].value(forKey: "view") as? UIView else {return}

then use like so:

let tab1CentreXanc = tab1frame.centerXAnchor
let tab1CentreY = tab1frame.centerYAnchor
let tab1FRAME = tab1frame (centerX and Y and other parameters such as ".origins.x or y"

Hope that helps.

If you want to reference the parameters from other tabs, just change the index from [0] to whatever you want

MFK
  • 37
  • 3
0

You do not need any private API, just use the UITabbar property itemWidth and itemSpacing. Set these two values like following:

NSInteger tabBar.itemSpacing = 10; tabBar.itemWidth = ([UIScreen mainScreen].bounds.size.width - self.tabBarController.tabBar.itemSpacing * (itemsCount - 1)) /itemsCount;

Note this will not impact the size or position of image and title used for UITabBarItem.

And then you can get the ith item's center x like following:

CGFloat itemCenterX = (tabBar.itemWidth + tabBar.itemSpacing) * ith + tabBar.itemWidth / 2;

Bigyelow
  • 51
  • 4
0

redead's answer definitely works so please upvote it.

I needed to add an activity indicator to the tabBar and to do that I needed the tabBar's rect inside the window so I had to add some additional code to his answer:

// 1. get the frame for the first tab like in his answer
guard let firstTab = tabBarController?.tabBar.items?[0].valueForKey("view") as? UIView else { return }
print("firstTabframe: \(firstTab.frame)")

// 2. get a reference to the window
guard let window = UIApplication.shared.keyWindow else { return }

// 3. get the firstTab's rect inside the window
let firstTabRectInWindow = firstTab.convert(firstTab.frame, to: window)
print("firstTabRectInWindow: \(firstTabRectInWindow)")
Lance Samaria
  • 17,576
  • 18
  • 108
  • 256