Figure out UIBarButtonItem frame in window?
Asked Answered
I

11

65

UIBarButtonItem does not extend UIView, so there is nothing like a frame property.

But is there any way I can get what is it's CGRect frame, relative to the application UIWindow?

Inharmonic answered 8/6, 2010 at 2:5 Comment(0)
P
126

Do you like to use private APIs? If yes,

UIView* view = thatItem.view;
return [view convertRect:view.bounds toView:nil];

Of course no one wants this when targeting the AppStore. A more unreliable method, and also uses undocumented features, but will pass Apple's test, is to loop through the subviews to look for the corresponding button item.

NSMutableArray* buttons = [[NSMutableArray alloc] init];
for (UIControl* btn in theToolbarOrNavbar.subviews)
  if ([btn isKindOfClass:[UIControl class]])
    [buttons addObject:btn];
UIView* view = [buttons objectAtIndex:index];
[buttons release];
return [view convertRect:view.bounds toView:nil];

The index is the index to your bar item in the array of .items, after removing all blank items. This assumes the buttons are arranged in increasing order, which may not be. A more reliable method is to sort the buttons array in increasing .origin.x value. Of course this still assumes the bar button item must inherit the UIControl class, and are direct subviews of the toolbar/nav-bar, which again may not be.


As you can see, there are a lot of uncertainty when dealing with undocumented features. However, you just want to pop up something under the finger right? The UIBarButtonItem's .action can be a selector of the form:

-(void)buttonClicked:(UIBarButtonItem*)sender event:(UIEvent*)event;

note the event argument — you can obtain the position of touch with

[[event.allTouches anyObject] locationInView:theWindow]

or the button view with

[[event.allTouches anyObject] view]

Therefore, there's no need to iterate the subviews or use undocumented features for what you want to do.

Pender answered 14/6, 2010 at 14:49 Comment(8)
I forgot about the event parameter. That's obviously the best choice.Sag
Beware those who use the approach with [[event.allTouches anyObject] view] you will get very strange results and sometimes crashes, here is why: try to click with 2 fingers the button AND some random view (even the system status bar at the top of the screen!), you will often get that random view instead of button! So I have changed my code to use presentPopoverFromBarButtonItem, which works flawlessly. If you absolutely need to get button's view, you may try to iterate through all touches looking for the view with particular superVieKirschner
Note that [[event.allTouches anyObject] doesn't work well if the user taps multiple places simultaneously. Instead you want to get the touch corresponding to the UITouchPhaseEnded state, as in: for( UITouch* touch in [event allTouches] ) { if( [touch phase] == UITouchPhaseEnded ) { view = [touch view]; } }Interphone
Where are the docs for the event parameter (Not UIEvent in general; rather, the event passed as an argument to the action)? I see the .action property, but nothing about parameters passed with the selector.Sharlenesharline
convertRect: is not private anymoreWelkin
I would also add that if you still want to find the location of the actual bar button view (assuming it exists) in the toolbar you can use hit test, e.g., CGPoint p = [[event.allTouches anyObject] locationInView:toolbar]; UIView *v = [toolbar hitTest:p withEvent:nil]; As of iOS 7 this works, but of course there's no guarantee that any descendant of UIView will be used in the future.Heller
Problem with event is the rotation. You can't track it or make necessary changes in layout. For me, best option is to create a UIButton and use it as custom view of UIBarButton.Legislative
This answer is now 5 years old. Is this really still the only way to get the frame of a UIBarButtonItem?Queenqueena
O
18

I didn't see this option posted (which in my opinion is much simpler), so here it is:

UIView *barButtonView = [barButtonItem valueForKey:@"view"];
Odelet answered 8/1, 2014 at 5:35 Comment(11)
valueForKey: is the only method being used. It is not a private API. All that is being done here is to make an assumption about Apple's implementation which Apple doesn't guarantee will stay the same.Franchescafranchise
@Franchescafranchise "view" is the private API here.Nemato
@bugloaf Apple doesn't seem to have a problem with "view" since I've shipped an app with it and made no attempts at all to hide what I was doing.Franchescafranchise
@Franchescafranchise You just didn't get caught. Apple's screening tools are not omniscient.Nemato
@bugloaf More likely there was nothing to catch since finding someone calling "view" with valueForKey: isn't very difficult.Franchescafranchise
valueForKey: comments here #11924097Franchescafranchise
@Franchescafranchise There is nothing in the binary that tells Apple's static analyzer whether this "view" property is one of the many public and well-documented view properties in iOS or whether it is a private API. As another example here is a blatant violation of Apple's prohibition against private API calls: https://mcmap.net/q/81829/-can-i-disable-uipickerview-scroll-sound but it's shipping in the App Store right now.Nemato
@bugloaf You point out well that you can get around Apple's checks. Your example is not "blatant". Blatant means completely obvious. If Apple doesn't have the source code how can it be obvious to Apple. In this case no one is trying to get around Apple's checks. "view" is explicitly stated and would show up as a string in the binary. The use of "view" as #11924097 points out just makes your code fragile.Franchescafranchise
@Franchescafranchise I think we just disagree on the definition of private API. My definition is any interface that Apple doesn't publicly document, even if the code that calls the interface happens to compile and Apple doesn't catch it.Nemato
@bugloaf Based on #11924097 your definition is wrong.Franchescafranchise
Hmmm, Meriam-Webster you are not.Franchescafranchise
C
8

In iOS 3.2, there's a much easier way to show an Action Sheet popover from a toolbar button. Merely do something like this:

- (IBAction)buttonClicked:(UIBarButtonItem *)sender event:(UIEvent *)event
{
 UIActionSheet *popupSheet;
 // Prepare your action sheet
 [popupSheet showFromBarButtonItem:sender animated:YES];
}
Clint answered 8/11, 2010 at 19:54 Comment(0)
C
4

This is the implementation I use for my WEPopover project: (https://github.com/werner77/WEPopover):

@implementation UIBarButtonItem(WEPopover)

- (CGRect)frameInView:(UIView *)v {

    UIView *theView = self.customView;
    if (!theView.superview && [self respondsToSelector:@selector(view)]) {
        theView = [self performSelector:@selector(view)];
    }

    UIView *parentView = theView.superview;
    NSArray *subviews = parentView.subviews;

    NSUInteger indexOfView = [subviews indexOfObject:theView];
    NSUInteger subviewCount = subviews.count;

    if (subviewCount > 0 && indexOfView != NSNotFound) {
        UIView *button = [parentView.subviews objectAtIndex:indexOfView];
        return [button convertRect:button.bounds toView:v];
    } else {
        return CGRectZero;
    }
}
@end
Candracandy answered 6/5, 2011 at 16:58 Comment(2)
Unfortunately, this at least sometimes does not work. I have a programmatically created UIBarButtonItems and I see that when customView is set to non-nil and then to nil, the bar button items disappears and [parentView.subviews objectAtIndex:indexOfView] crashes (indexOfView not less than subviews.count).Ductile
I edited the answer with the current (and working) implementationCandracandy
T
4

As long as UIBarButtonItem (and UITabBarItem) does not inherit from UIView—for historical reasons UIBarItem inherits from NSObject—this craziness continues (as of this writing, iOS 8.2 and counting ... )

The best answer in this thread is obviously @KennyTM's. Don't be silly and use the private API to find the view.

Here's a oneline Swift solution to get an origin.x sorted array (like Kenny's answer suggests):

    let buttonFrames = myToolbar.subviews.filter({
        $0 is UIControl
    }).sorted({
        $0.frame.origin.x < $1.frame.origin.x
    }).map({
        $0.convertRect($0.bounds, toView:nil)
    })

The array is now origin.x sorted with the UIBarButtonItem frames.

(If you feel the need to read more about other people's struggles with UIBarButtonItem, I recommend Ash Furrow's blog post from 2012: Exploring UIBarButtonItem)

Tailing answered 19/11, 2014 at 19:49 Comment(0)
H
2

I was able to get Werner Altewischer's WEpopover to work by passing up the toolbar along with the
UIBarButton: Mod is in WEPopoverController.m

- (void)presentPopoverFromBarButtonItem:(UIBarButtonItem *)item toolBar:(UIToolbar *)toolBar
               permittedArrowDirections:(UIPopoverArrowDirection)arrowDirections 
                               animated:(BOOL)animated 
{
    self.currentUIControl = nil;
    self.currentView = nil;
    self.currentBarButtonItem = item;
    self.currentArrowDirections = arrowDirections;
    self.currentToolBar = toolBar;

    UIView *v = [self keyView];
    UIButton *button = nil;

    for (UIView *subview in toolBar.subviews) 
    {
        if ([[subview class].description isEqualToString:@"UIToolbarButton"])
        {
            for (id target in [(UIButton *)subview allTargets]) 
            {
                if (target == item) 
                {
                    button = (UIButton *)subview;
                    break;
                }
            }
            if (button != nil) break;
        }
    }

    CGRect rect = [button.superview convertRect:button.frame toView:v];

    [self presentPopoverFromRect:rect inView:v permittedArrowDirections:arrowDirections animated:animated];
}
Hofmann answered 2/2, 2012 at 10:36 Comment(0)
T
2
-(CGRect) getBarItemRc :(UIBarButtonItem *)item{
    UIView *view = [item valueForKey:@"view"];
    return [view frame];
}
Teamster answered 13/1, 2015 at 7:21 Comment(0)
S
1

You can get it from the UINavigationBar view. The navigationBar is a UIView which has 2 or 3 custom subviews for the parts on the bar.

If you know that the UIBarButtonItem is currently shown in the navbar on the right, you can get its frame from navbar's subviews array.

First you need the navigationBar which you can get from the navigationController which you can get from the UIViewController. Then find the right most subview:

UINavigationBar* navbar = curViewController.navigationController.navigationBar;
UIView* rightView = nil;

for (UIView* v in navbar.subviews) {
   if (rightView==nil) {
      rightView = v;
   } else if (v.frame.origin.x > rightView.frame.origin.x) {
      rightView = v;  // this view is further right
   }
}
// at this point rightView contains the right most subview of the navbar

I haven't compiled this code so YMMV.

Sag answered 14/6, 2010 at 14:15 Comment(0)
F
0

This is not the best solution and from some point of view it's not right solution and we can't do like follow because we access to object inside UIBarBattonItem implicitly, but you can try to do something like:

UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 30, 30)];
[button setImage:[UIImage imageNamed:@"Menu_Icon"] forState:UIControlStateNormal];
[button addTarget:self action:@selector(didPressitem) forControlEvents:UIControlEventTouchUpInside];
UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithCustomView:button];
self.navigationItem.rightBarButtonItem = item;

CGPoint point = [self.view convertPoint:button.center fromView:(UIView *)self.navigationItem.rightBarButtonItem];
//this is like view because we use UIButton like "base" obj for 
//UIBarButtonItem, but u should note that UIBarButtonItem base class
//is NSObject class not UIView class, for hiding warning we implicity
//cast UIBarButtonItem created with UIButton to UIView
NSLog(@"point %@", NSStringFromCGPoint(point));

as result i got next:

point {289, 22}

enter image description here

Flirtatious answered 10/3, 2016 at 21:32 Comment(1)
Using the event parameter from the tap action is still the safest way to grab the frame for when a tap occurred. But if you need to figure out the frame without detecting taps, I'd say this is a nice solution if you check rightBarButtonItem for respondsToSelector.Inharmonic
R
-1

Before implement this code, be sure to call [window makeKeyAndVisible] in your Applition delegate application:didFinishLaunchingWithOptions: method!

- (void) someMethod
{
    CGRect rect = [barButtonItem convertRect:barButtonItem.customview.bounds toView:[self keyView]];
}

- (UIView *)keyView {
UIWindow *w = [[UIApplication sharedApplication] keyWindow];
if (w.subviews.count > 0) {
    return [w.subviews objectAtIndex:0];
} else {
    return w;
}
}
Redbird answered 11/9, 2012 at 20:23 Comment(2)
UIBarButtonItem instances don't have a bounds property.Valery
The code does not access the bounds of the barButtonItem. It is accessing the bounds of the custom view inside the barButtonItem.Upi
U
-1

I handled it as follows:

- (IBAction)buttonClicked:(UIBarButtonItem *)sender event:(UIEvent *)event
{
    UIView* view = [sender valueForKey:@"view"]; //use KVO to return the view
    CGRect rect = [view convertRect:view.bounds toView:self.view];
    //do stuff with the rect
}
Upi answered 14/8, 2014 at 13:53 Comment(1)
"-valueForKey: is a documented, public method. The KVC API provides a way to prevent direct instance variable access, so if the value is accessible, then no action was taken to bar access. One concern might be hitting on an undefined value in a future version of iOS. To address that concern, you could wrap the call in a try-block. Also, even if you feel you can't use this in an app destined for the App Store, that doesn't mean someone targeting jailbroken devices couldn't use this approach in their application. " written by user @jeremy-w-sherman in another postUpi

© 2022 - 2024 — McMap. All rights reserved.