Welcome to my personal website: www.sketchk.xyz

The overview

After iOS 11, Apple enabled the feature of automatic layout in the navigation bar, which caused some changes in the way the navigation bar is used. Today, we will focus on several changes of UIBarButtonItem in the navigation bar in iOS 11.

Major changes

  • Changes at the view level
  • Click on the region change
  • Changes with screen spacing

View level changes

form

Updating Your App for iOS 11 in WWDC 2018, we can see that UINavigationBar now supports Auto Layout.

So what does that mean for UIBarButtonItem? With view Debug we can see that all items are managed by a built-in Stack View.

When the Custom View properly implements sizeThatFits or intrinsicContentSize, the UI presentation will not have problems.

Matters needing attention

In iOS 11, to take full advantage of the Auto Layout feature, Not need to be UIBarButtonItem Custom in the View of translatesAutoresizingMaskIntoConstraints attribute is set to no, this may be the cause of it under the iOS 11 layout disorder occurred in the system, So we need to write the following code in place.

UIView *view = [UIView new];
if(@available(iOS 11, *)){
  view.translatesAutoresizingMaskIntoConstraints = NO;
}
UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithCustomView:view];
self.navigationItem.rightBarButtonItem = item;
Copy the code

Click on the region change

form

We create two UIBarButtonItems in the form of a Custom View, and use the View Debug tool to see the true size of the Custom View

In versions prior to iOS 10, the click area is shown in red:

The solution

There are two possible solutions to this problem:

One way is to rewrite the – (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event method to modify the click area of the control to ensure that its range is controlled above 44 * 44 pt. For example:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
    CGSize acturalSize = self.frame.size;
    CGSize minimumSize = kBarButtonMinimumTapAreaSize; // 44 * 44 pt
    CGFloat verticalMargin = acturalSize.height - minimumSize.height >= 0 ? 0 : ((minimumSize.height - acturalSize.height ) / 2);
    CGFloat horizontalMargin = acturalSize.width - minimumSize.width >= 0 ? 0 : ((minimumSize.width - acturalSize.width ) / 2);
    CGRect newArea = CGRectMake(self.bounds.origin.x - horizontalMargin, self.bounds.origin.y - verticalMargin, self.bounds.size.width + 2 * horizontalMargin, self.bounds.size.height + 2 * verticalMargin);
    return CGRectContainsPoint(newArea, point);
}
Copy the code

Another way to do this is to create a mid-tier view that is at least 44 * 44 in size, as shown in the following code:

@interface WrapperView : UIView
@property (nonatomic, assign) CGSize minimumSize;
@property (nonatomic, strong) UIView *underlyingView;
@end
@implementation WrapperView
- (instancetype)initWithUnderlyingView:(UIView *)underlyingView {
    self = [super initWithFrame:underlyingView.bounds];
    if(self) { _underlyingView = underlyingView; _minimumSize = CGSizeMake (44.0 44.0 f, f); underlyingView.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:underlyingView]; NSLayoutConstraint *leadingConstraint = [underlyingView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor]; NSLayoutConstraint *trailingConstraint = [underlyingView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor]; NSLayoutConstraint *topConstraint = [underlyingView.topAnchor constraintEqualToAnchor:self.topAnchor]; NSLayoutConstraint *bottomConstraint = [underlyingView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor]; NSLayoutConstraint *heightConstraint = [self.heightAnchor constraintGreaterThanOrEqualToConstant:self.minimumSize.height]; NSLayoutConstraint *widthConstraint = [self.widthAnchor constraintGreaterThanOrEqualToConstant:self.minimumSize.width]; [NSLayoutConstraint activateConstraints:@[leadingConstraint, trailingConstraint, topConstraint, bottomConstraint, heightConstraint, widthConstraint]]; }return self;
}
@end
Copy the code

Of course, the easiest way to do this is to make your image bigger, like increase the white space…

Changes with screen spacing

Before iOS 11, we had two ways to change the spacing between items and the screen

  • Set a broadband item of type Fixed Space to a negative value
  • Override the alignmentRectInsets method of Custom View
  • If the Custom View for UIButton types can override the contentEdgeInsets/imageEdgeInsets/titleEdgeInsets and modify the hitTest area

Solution 1 in iOS 10: Use items with a negative width and type Fixed Space

When we use a Custom View item, the default margin between the item and the screen is 16 or 20 pt (PS: 20 pt when the screen is 5.5-inch). The following figure takes 16 pt as an example:

UIBarButtonItem *spacer = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
spacer.width = -8;
self.navigationItem.rightBarButtonItems = @[spacer, item1, item2];
Copy the code

This scheme works on iOS 10 and below, as shown below:

But the same code doesn’t work on iOS 11…

A careful study of the View layout in iOS 11 shows that the behavior of items of fixed space type in leading and trailling of Stack View is consistent with that between items, and its minimum width is 8pt. This explains why setting negative numbers doesn’t work.

Solution 2 in iOS 10: Rewrite the Custom View’s alignmentRectInsets method

We can override the alignmentRectInsets property of a view control, as in the following code:

- (UIEdgeInsets)alignmentRectInsets {
    return self.position == right ? UIEdgeInsetsMake(0, -8, 0, 8) : UIEdgeInsetsMake(0, 8, 0, -8);
}
Copy the code

When we set UIEdgeInsetsMake(0, -8, 0, 8) and apply this code to the previous example, you should see something like this:

At first glance, this approach seems to solve all the problems, but if you look closely, you’ll see that there are some imperfections, such as the part of the stack View that will not respond to click events

Solution 3 in iOS 10: Use the XXXEdgeInsets property to change it

Scheme 3 and scheme 2 are similar, by modifying the XXXEdgeInsets property can actually make the control in the visual expectations, but because of the existence of stack View, even if the Custom View hitTest area is modified, there will be unable to respond to the click event problem. So this isn’t a perfect solution:

The solution

While none of the three solutions mentioned above will work perfectly in iOS 11, we did find one interesting thing

The following figure shows the layout of the view using an item with a negative width and Fixed Space type. Although Fixed Space makes it look a little far from the screen margin visually, the margin between the Item control and the screen does become smaller.

This may seem confusing, but let’s look at the difference. Here is a UIBarButtonItem created using a non-CustomView with a screen spacing of 8 pt (12 pt when the screen is 5.5 inches).

UIButton *customButton = [UIButton buttonWithType:UIButtonTypeCustom];
customButton.overrideAlignmentRectInsets = UIEdgeInsetsMake(0, x, 0, -x); // you should do this inyour own custom class customButton.translatesAutoresizingMaskIntoConstraints = NO; UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithCustomView:customButton] UIBarButtonItem *positiveSeparator = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil]; positiveSeparator.width = 8; self.navigationItem.leftBarButtonItems = @{positiveSeparator, item, ... }Copy the code

conclusion

The changes to UIBarButtonItem on iOS 11 have caused quite a bit of headaches for developers, and a number of solutions we’ve seen on Apple Developer Forums have generally been to make changes to NavigationBar, such as change constraints, Implement a navigation bar base class and so on, but for our existing projects, these solutions are too expensive to implement.

The solution proposed in this article does not require any modification to the navigation bar itself, but only requires some processing on UIBarButtonItem. Compared with the existing solutions in the community, this adaptation solution is worth trying for projects with heavy historical burdens.