73

I have a button in a toolbar. How can I grab its frame? Do UIBarButtonItems not have a frame property?

Luca Davanzo
  • 21,000
  • 15
  • 120
  • 146
totalitarian
  • 3,606
  • 6
  • 32
  • 55
  • 3
    That's because `UIBarButtonItems` are not `UIViews`. Why do you need the frame? maybe there is a better approach – Ismael Jan 14 '13 at 12:28
  • basically i'm using a this https://github.com/Ciechan/BCGenieEffect#bcgenieeffect and trying to simulate a view 'sucking' into a button on my toolbar – totalitarian Jan 14 '13 at 12:59

15 Answers15

94

Try this one;

UIBarButtonItem *item = ... ;
UIView *view = [item valueForKey:@"view"];
CGFloat width;
if(view){
    width=[view frame].size.width;
}
else{
    width=(CGFloat)0.0 ;
}
Anoop Vaidya
  • 46,283
  • 15
  • 111
  • 140
  • 6
    There is no definition of "Appstore safe". Your program is not crashing, you are not hacking, you are not invalidating anything that apple doesn't want then its fine. Many a times apps are rejected for strange reason. – Anoop Vaidya Jan 14 '13 at 13:08
  • 3
    OK, I saw mentioned before that [item valueForKey:@"view"] is using an undocumented API method... – totalitarian Jan 14 '13 at 13:13
  • Or you can post another question on that... can we use that undoc api... :) – Anoop Vaidya Jan 14 '13 at 13:16
  • I found an alternative method that looks a little safer, i'll still mark you correct for this question though. Thanks again – totalitarian Jan 14 '13 at 13:19
  • 1
    And please post your answer also. So that people searching will find few answers and go for better answer. – Anoop Vaidya Jan 14 '13 at 13:21
  • 28
    "There is no definition of Appstore safe". That statement is invalid. Using the private API is NOT "Appstore safe". Period – aryaxt Jun 02 '15 at 21:41
  • @aryaxt: I said wrt to this answer. And for other cases yes you are correct. – Anoop Vaidya Mar 17 '16 at 16:48
  • HI @AnoopVaidya I wanted to know how to get more information regarding the undocumented APIs that are been used in the UIKit user interfaces? – LearneriOS Oct 22 '16 at 19:00
  • 1
    @LearneriOS: The more you code, the more you learn. Once you start diving you will come to know obj-c runtime & also wwdc and other legacy codes...etc will help you learn more *undocumented* things. – Anoop Vaidya Oct 23 '16 at 12:14
  • Does this works only with any specific iOS versions? – Praveenkumar Dec 21 '16 at 10:46
  • @PoolHallJunkie Try out the method used in this answer: https://stackoverflow.com/a/46965131/4980464 – Dan Stenmark Oct 30 '17 at 06:11
  • and? tried to get frame for `rightBarButtonItem`. Your way returns origin (0,0). Downvoted – Vyachaslav Gerchicov Nov 24 '17 at 08:15
20

This way works best for me:

UIView *targetView = (UIView *)[yourBarButton performSelector:@selector(view)];
CGRect rect = targetView.frame;
MobileMon
  • 8,341
  • 5
  • 56
  • 75
  • I really like your solution, for it's simple and based on the identifier of the button directly. It works well, however it seems its frame is larger than expected. I'm using it to calculate the position of a `PopOver`, and it's placed a little low beneath the button. Is this related to your solution, and if so, do you have a(nother) solution for this? Thanks in advance! – Tum Feb 14 '14 at 13:07
  • @Tumtum sounds like an issue with how the popover is added. Add the popover to self.navigationController.view instead of self.view – MobileMon Feb 14 '14 at 13:32
  • @Turnturn either that or just subtract some pixels from the CGRect's y origin: rect.origin.y = rect.origin.y - 5; – MobileMon Feb 14 '14 at 13:42
  • You're a lifesaver! I used a combination of both of your solutions as the popover was positioned a little too high using just the first option you gave me, so I corrected that using the second option. Thanks for taking the time to answer my subquestion. – Tum Feb 15 '14 at 13:23
17

Oof, lots of rough answers in this thread. Here's the right way to do it:

import UIKit

class ViewController: UIViewController {

    let customButton = UIButton(type: .system)

    override func viewDidLoad() {
        super.viewDidLoad()

        customButton.setImage(UIImage(named: "myImage"), for: .normal)
        self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: customButton)
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        print(self.customButton.convert(self.customButton.frame, to: nil))
    }
}
Dan Stenmark
  • 591
  • 6
  • 11
16

With Swift, if you needs to often work with bar button items, you should implement an extension like this:

extension UIBarButtonItem {

    var frame: CGRect? {
        guard let view = self.value(forKey: "view") as? UIView else {
            return nil
        }
        return view.frame
    }

}

Then in your code you can access easily:

if let frame = self.navigationItem.rightBarButtonItems?.first?.frame {
    // do whatever with frame            
}
Luca Davanzo
  • 21,000
  • 15
  • 120
  • 146
12

Thanks to Anoop Vaidya for the suggested answer. An alternative could be (providing you know the position of the button in the toolbar)

UIView *view= (UIView *)[self.toolbar.subviews objectAtIndex:0]; // 0 for the first item


CGRect viewframe = view.frame;
totalitarian
  • 3,606
  • 6
  • 32
  • 55
  • This is good way, cant u find Index by looping all the views and chekcing some tag or outlet name? – Anoop Vaidya Jan 14 '13 at 13:24
  • Thanks. Good idea about the loop, but for my situation the I know the index so a loop wouldn't be required. – totalitarian Jan 14 '13 at 14:11
  • in the case...wen u dont knoe the index...then u can find it using loop. it will be a general code for all. – Anoop Vaidya Jan 14 '13 at 15:56
  • Also keep in mind that `subviews` might be empty, in which case the code would lead to a crash. So in a shipping application you should check for that. – de. May 07 '13 at 14:35
  • 1
    The order of the subviews changes for iOS 6, right? the first button is now the last one and viceversa – aprunedamtz Jun 06 '13 at 16:48
4

Here's what I'm using in iOS 11 & Swift 4. It could be a little cleaner without the optional but I'm playing it safe:

extension UIBarButtonItem {
    var view: UIView? {
        return perform(#selector(getter: UIViewController.view)).takeRetainedValue() as? UIView
    }
}

And usage:

if let barButtonFrame = myBarButtonItem.view?.frame {
    // etc...
}

Edit: I don't recommend using this anymore. I ended up changing my implementation to use UIBarButtonItems with custom views, like Dan's answer

jday
  • 578
  • 6
  • 14
  • I really liked this answer, and used it in a production setting. Sadly, it caused the occasional crash by objc_retain. – Ron Regev Sep 22 '21 at 19:48
  • Perhaps use takeUnretainedValue()? Is the risk of a memory leak here greater than that of a crash? – Ron Regev Sep 22 '21 at 20:37
  • @RonRegev see edit. I ended up using custom views. It was more work, but a better and safer solution in the end. Good luck! – jday Oct 03 '21 at 22:08
3
-(CGRect) getBarItemRc :(UIBarButtonItem *)item{
    UIView *view = [item valueForKey:@"view"];
    return [view frame];
}
Gank
  • 4,507
  • 4
  • 49
  • 45
1

You can create a UIBarButtonItem with a custom view, which is a UIButton, then you can do whatever you want. :]

Nix Wang
  • 814
  • 10
  • 18
1

in Swift 4.2 and inspired with luca

extension UIBarButtonItem {

    var frame:CGRect?{
        return (value(forKey: "view") as? UIView)?.frame
    }

}


guard let frame = self.navigationItem.rightBarButtonItems?.first?.frame else{ return }
Reimond Hill
  • 4,278
  • 40
  • 52
1

You can roughly calculate it by using properties like layoutMargins and frame on the navigationBar, combined with icon size guides from Human Interface Guidelines and take into count the current device orientation:

- (CGRect)rightBarButtonFrame {
    CGFloat imageWidth = 28.0;
    CGFloat imageHeight = UIDevice.currentDevice.orientation == UIDeviceOrientationLandscapeLeft || UIDevice.currentDevice.orientation == UIDeviceOrientationLandscapeRight ? 18.0 : 28.0;
    UIEdgeInsets navigationBarLayoutMargins = self.navigationController.navigationBar.layoutMargins;
    CGRect navigationBarFrame = self.navigationController.navigationBar.frame;
    return CGRectMake(navigationBarFrame.size.width-(navigationBarLayoutMargins.right + imageWidth), navigationBarFrame.origin.y + navigationBarLayoutMargins.top, imageWidth, imageHeight);
}
turingtested
  • 6,356
  • 7
  • 32
  • 47
0

Try this implementation:

@implementation UIBarButtonItem(Extras)

- (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
Werner Altewischer
  • 10,080
  • 4
  • 53
  • 60
  • This has the same disadvantage as the other answers (the undocumented selector "view" needs to be defined), and it does a totally pointless lookup in the superview. – fishinear Aug 06 '20 at 18:08
0

You should do a loop over the subviews and check their type or their contents for identifying. It is not safe to access view by kvo and you cannot be sure about the index.

0

Check out this answer: How to apply borders and corner radius to UIBarButtonItem? which explains how to loop over subviews to find the frame of a button.

Community
  • 1
  • 1
frandogger
  • 314
  • 2
  • 9
0

I used a view on the bar button item with a tag on the view:

for view in bottomToolbar.subviews {
   if let stackView = view.subviews.filter({$0 is UIStackView}).first {
       //target view has tag = 88
       if let targetView = stackView.subviews.filter({$0.viewWithTag(88) != nil}).first {                                
          //do something with target view
   }
}

}

dawid
  • 374
  • 5
  • 9
-2

Swift 4 up The current best way to do it is to access its frame from :

self.navigationItem.rightBarButtonItems by

let customView = navigationItem.rightBarButtonItems?.first?.customView // access the first added customView

Accessing this way is safer than accessing private api.

  • check out the answer in this :

After Add a CustomView to navigationItem, CustomView always return nil

vhong
  • 594
  • 4
  • 27