Getting the frame of UIBarButtonItem
In this post, I want to share my experience of finding the frame of UIBarButtonItem
.
You probably already know that unfortunately UIBarButtonItem
doesn’t subclass the UIView
class and doesn’t contain frame
property. There are some historical reasons for that. Therefore developers who need to get frame of UIBarButtonItem
are using different hacks or ways to accomplish this task.
I couldn’t find the one solution which fully solves my problems. Therefore in the below paragraphs, you can find my approach for this uncommon issue and which is working on iOS 11.
UIViewController
extension which I used to get the frame of UIBarButtonItem
in UINavigationBar
or UITolbar
:
/* In UIViewController+UIBarButtonItemFrame.h */
#import <UIKit/UIKit.h>
#define SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending)
@interface UIViewController (UIBarButtonItemFrame)
- (CGRect)frameForNavBarItem:(UIBarButtonItem *)item;
- (CGRect)frameForToolbarItem:(UIBarButtonItem *)item flexibleSpaceItem:(UIBarButtonItem *)flexibleSpaceItem;
@end
/* In UIViewController+UIBarButtonItemFrame.m */
#import "UIViewController+UIBarButtonItemFrame.h"
@implementation UIViewController (Extensions)
- (CGRect)frameForView:(UIView *)view {
return [view convertRect:view.bounds toView:nil];;
}
- (void)buttonsForView:(UIView *)view buttons:(NSMutableArray<UIView *> *)buttons {
for (UIView *subview in view.subviews) {
if ([subview isKindOfClass:[UIControl class]]) {
[buttons addObject:subview];
} else {
[self buttonsForView:subview buttons:buttons];
}
}
}
- (NSArray<UIControl *> *)buttonsForView:(UIView *)view {
NSMutableArray *buttons = [[NSMutableArray alloc] init];
[self buttonsForView:view buttons:buttons];
if (SYSTEM_VERSION_LESS_THAN(@"11.0")) {
[buttons sortUsingComparator:^NSComparisonResult(UIControl *obj1, UIControl *obj2) {
if (obj1.frame.origin.x > obj2.frame.origin.x) {
return NSOrderedDescending;
} else if (obj1.frame.origin.x < obj2.frame.origin.x) {
return NSOrderedAscending;
}
return NSOrderedSame;
}];
}
return buttons;
}
- (CGRect)frameForNavBarItem:(UIBarButtonItem *)item {
NSArray<UIControl *> *buttons = [self buttonsForView:self.navigationController.navigationBar];
NSMutableArray *items = [NSMutableArray new];
[items addObjectsFromArray:self.navigationItem.leftBarButtonItems];
[items addObjectsFromArray:[[self.navigationItem.rightBarButtonItems reverseObjectEnumerator] allObjects]];
NSUInteger index = [items indexOfObject:item];
if (index < buttons.count) {
UIView *view = buttons[index];
return [self frameForView:view];
}
return [self frameForView:self.navigationController.navigationBar];
}
- (CGRect)frameForToolbarItem:(UIBarButtonItem *)item flexibleSpaceItem:(UIBarButtonItem *)flexibleSpaceItem {
NSArray<UIControl *> *buttons = [self buttonsForView:self.navigationController.toolbar];
NSMutableArray *toolbarItems = [NSMutableArray arrayWithArray:self.toolbarItems];
NSMutableArray *itemsToRemove = [NSMutableArray new];
for (UIBarButtonItem *barButtonItem in toolbarItems) {
if (barButtonItem == flexibleSpaceItem) {
[itemsToRemove addObject:barButtonItem];
}
}
[toolbarItems removeObjectsInArray:itemsToRemove];
NSUInteger index = [toolbarItems indexOfObject:item];
if (index < buttons.count) {
UIView *view = buttons[index];
return [self frameForView:view];
}
return [self frameForView:self.navigationController.toolbar];
}
@end
- (CGRect)frameForView:(UIView *)view;
- This method is straightforward. It returns the frame ofUIView
in device screen space.- (void)buttonsForView:(UIView *)view buttons:(NSMutableArray<UIView *> *)buttons;
- This method takesUINavigationBar
orUIToolbar
instance as a parameter and then iterates through their subviews until finds all subviews ofUIControl
subclass. In other words, this method will find all views ofUIBarButtonItem
s.- (void)buttonsForView:(UIView *)view
- This method calls the previous one and returns the list of views in sorted order by x-axis.- (CGRect)frameForNavBarItem:(UIBarButtonItem *)item
- This method takes a parameter of typeUIBarButtonItem
, then calls our third method to get all views ofUIBarButtonItem
s ofUINavigationBar
. After the method combinesleftBarButtonItems
andrightBarButtonItems
to one array. Note thatrightBarButtonItems
are reversed, this decision was made after testing. Then the method simply tries to get the index of inputUIBarButtonItem
instance in the combined array, and if it is found and the index is smaller than the size of the array of views ofUIBarButtonItem
s inUINavigationBar
the method returns the corresponding view. Otherwise, the frame of wholeUINavigationBar
is returned.- (CGRect)frameForToolbarItem:(UIBarButtonItem *)item flexibleSpaceItem:(UIBarButtonItem *)flexibleSpaceItem
- This method is almost identical to 3rd one. The difference is that it takesUIToolbar
to find views ofUIBarButtonItem
s, and also removes unneededUIBarButtonItem
s fromtoolbarItems
. In my case, it wasUIBarButtonItem
with the type ofUIBarButtonSystemItemFlexibleSpace
. You can have another one(s).
Another thing to note is that I had a logic where UINavigationBar
or UIToolbar
items were changed over time. When I called the above methods after changing UIBarButtonItem
list I noticed that the views were not created immediately (or maybe had zero frames). Therefore in that scenario, you will want to call methods to update the layout of UINavigationBar
or UIToolbar
before calling above methods. For example, like this:
[self setToolbarItems:SOME_ITEMS animated:false];
[self.navigationController.toolbar setNeedsDisplay];
[self.navigationController.toolbar setNeedsLayout];
[self.navigationController.toolbar layoutIfNeeded];
// then get frames
So, that was a description of my solution. It works nicely to solve my problems. I hope that solution will give you an idea of how to approach the problem of finding UIBarButtonItem
frame.
Leave a Comment