【问题标题】:Getting the frame of a particular tab bar item获取特定标签栏项目的框架
【发布时间】:2011-09-13 14:30:39
【问题描述】:

有没有办法在UITabBar 中找到特定UITabBarItem 的框架?

具体来说,我想创建一个图像“落入”其中一个选项卡的动画,类似于例如删除邮件中的电子邮件,或在 iTunes 应用程序中购买曲目。所以我需要动画的目标坐标。

据我所知,没有公共 API 可以获取坐标,但我很想弄错。除此之外,我将不得不使用给定标签相对于标签栏框架的索引来猜测坐标。

【问题讨论】:

  • 嘿,你有没有在 swift 中找到解决方案。如果是,那么你能帮帮我吗..

标签: iphone ios uitabbarcontroller uitabbaritem


【解决方案1】:

Imre 的实现缺少一些重要的细节。

  1. UITabBarButton 视图不一定按顺序排列。例如,如果您在 iPhone 上有超过 5 个选项卡并重新排列了选项卡,则视图可能会乱序。
  2. 如果您使用超过 5 个选项卡,越界索引仅表示该选项卡位于“更多”选项卡的后面。在这种情况下,没有理由因为断言而失败,只需使用最后一个选项卡的框架。

所以我稍微改变了他的代码,然后我想出了这个:

+ (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;
}

【讨论】:

  • 鉴于 UITabBarButton 是一个私有的 iOS 类,并且 Apple 的 AppStore 审查过程越来越彻底,我认为谨慎的做法是根本不引用该类——即使是通过 NSClassFromString()。但它是一个 UIControl 子类,所有 UIControl 都实现了 -(CGRect)frame。所以你可以用这个消除一堆条件:if ([view isKindOfClass:[UIControl class]]) [tabBarItems addObject:view];
  • 这似乎在旋转设备后返回不正确的结果。我从纵向旋转到横向,但它返回纵向值。当我从横向旋转到纵向时,它会返回横向值。我在viewDidLayoutSubviews 中运行此代码。有什么想法吗?
  • 在 iOS 10 中,断言受到了打击。似乎所有子视图的原点为零,发生这种情况时为零,因此视图尚未布置。我可能会等到它完成所有布局后再调用此方法。
【解决方案2】:

另一个可能但草率的解决方案如下:

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

【讨论】:

    【解决方案3】:

    与 UITabBar 中的标签栏项目关联的子视图属于 UITabBarButton 类。通过使用两个选项卡记录 UITabBar 的子视图:

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

    你得到:

    <_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>>
    

    基于此,解决方案是微不足道的。我写的试用方法如下:

    + (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;
    }
    

    需要注意的是,UITabBar 的结构(子视图)和类 UITabBarButton 本身不是公共 API 的一部分,因此理论上它可以在任何新的 iOS 版本中更改而无需事先通知。尽管如此,他们不太可能更改这些细节,而且它适用于 iOS 5-6 及之前的版本。

    【讨论】:

    • ipad +1,但此解决方案不适用于 iPhone/ipod。这给出了错误的坐标。
    • 在装有 iOS 9.2 的 iPhone 上运行良好。相比之下,我使用UIControl(@Greg Combs 建议) 作为一种子视图类。
    【解决方案4】:

    在 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
    }
    

    【讨论】:

    • 这在 iPhone 上效果很好,但在 iPad 上似乎无法正常工作。在 iPad 上,项目(或至少 - 图像)被放置在中心,而这会返回同样宽的框架。
    • @KlimczakM 这应该返回实际项目的框架——如果你知道图像大小,你可以在flatMap调用中创建给定大小的CGRects,以view.center为中心?跨度>
    【解决方案5】:

    通过扩展 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
        }
    
    }
    

    【讨论】:

      【解决方案6】:

      标签栏按钮是启用用户交互的唯一子视图。检查这个而不是 UITabBarButton 以避免违反隐藏的 API。

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

      进入数组后,根据原点 x 对其进行排序,您将拥有标签栏按钮的真实顺序。

      【讨论】:

        【解决方案7】:

        我将在我的简单 UITabBarController 场景中添加对我有用的内容,一切都是合法的,但它假设项目的间隔相等。在 iOS7 下,它返回一个 UITabBarButton 的实例,但如果您将它用作 UIView*,您真的不在乎它是什么,也没有明确说明该类。返回视图的框架是您要查找的框架:

        -(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 ];
        }
        

        它的作用是计算按钮所在的 UITabBar 中的矩形,找到该矩形的中心并通过 hitTest:withEvent 挖掘出该点的视图。

        【讨论】:

        • 很棒的发现!这要少得多。
        • 它不适用于iPad,因为每个项目的宽度不等于标签栏的宽度除项目数
        【解决方案8】:

        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)
        }
        

        【讨论】:

          【解决方案9】:

          斯威夫特 5:

          只需将它放在处理 tabBar 的视图控制器中即可。

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

          然后像这样使用:

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

          希望对您有所帮助。

          如果你想引用其他选项卡中的参数,只需将索引从 [0] 更改为你想要的任何值

          【讨论】:

            【解决方案10】:

            您不需要任何私有 API,只需使用 UITabbar 属性 itemWidthitemSpacing。设置这两个值,如下所示:

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

            请注意,这不会影响用于 UITabBarItem 的图像和标题的大小或位置。

            然后你可以得到ith项目的中心x,如下所示:

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

            【讨论】:

              【解决方案11】:

              redead's 答案绝对有效,所以请点赞。

              我需要向 tabBar 添加一个活动指示器,为此我需要窗口内的 tabBar 矩形,所以我必须在他的答案中添加一些额外的代码:

              // 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)")
              

              【讨论】:

                【解决方案12】:

                初始化标签栏时,给它一个标签:

                let tabItem = UITabBarItem(title: "my title", image: nil, tag: 1000)

                然后您可以使用以下方法获取标签栏:

                let myTabItem = tabBarController?.tabBar.viewWithTag(1000)

                【讨论】: