UISearchBar increases navigation bar height in iOS 11
UISearchBar increases navigation bar height in iOS 11
I have my UISearchBar
being part of the navigation bar like:
UISearchBar
let searchBar = UISearchBar()
//some more configuration to the search bar
.....
navigationItem.titleView = searchBar
After updating to iOS 11
something weird happened to the search bar in my app. On iOS 10
and prior I had my navigation bar looking like:
iOS 11
iOS 10
Now with iOS 11
I have:
iOS 11
As you can see there is difference in the rounding of the two search bars which does not bothers me. The problem is that the search bar increases the height of the navigation bar. So when I go to another controller it looks weird too:
In fact that weird black line's height plus the current navigation bar's height is equal to the height of navigation bar shown in the second picture ...
Any ideas how to get rid of the black line and having consistent navigation bar height across all view controllers ?
15 Answers
15
You can add a constraint of height 44 to the search bar for iOS 11.
if #available(iOS 11.0, *) {
searchBar.heightAnchor.constraint(equalToConstant: 44).isActive = true
}
// Objective-c
if (@available(iOS 11.0, *)) {
[searchBar.heightAnchor constraintEqualToConstant:44].active = YES;
}
Although this works for bringing the height back to normal, if I tap on my searchbar, for some reason it cuts off the bottom part of the searchbar. Any ideas why this happens or anyone else having this issue?
– Christopher Smit
Sep 29 '17 at 12:20
Here's the obj-c version: [searchBar.heightAnchor constraintEqualToConstant:44].active = YES; but one issue is that while this fixes the height issue, it reeks havoc on the rest of UISearchController's view. E.g., the bar buttons are hidden at first and the magnifying glass looks half drawn, or as if something is on top of it. Anyone else?
– Bill Noto
Oct 5 '17 at 21:35
Totally weird looking if you use this code because it just narrows the navigation bar but the height of the search bar is still 56.
– OtakuFitness
Oct 12 '17 at 6:09
Did anyone else find a solution? doing this creates a lot of weird behavioral issues with the search bar.
– Gustavo_fringe
Oct 17 '17 at 15:30
This is not enough answer. When you click on the field, the size is still messed up.
– Ross
Oct 21 '17 at 19:48
I believe in iOS 11 UISearchBar now has the height equals to 56, and UINavigationBar uses autolayout to fit its subviews hence it increases the height. If you still want to have UISearchBar as titleView as in pre-iOS 11, I found out the best way to do it is to embed UISearchBar in a custom view, and set this view's height to 44, and assign it to navigationItem.titleView
class SearchBarContainerView: UIView {
let searchBar: UISearchBar
init(customSearchBar: UISearchBar) {
searchBar = customSearchBar
super.init(frame: CGRect.zero)
addSubview(searchBar)
}
override convenience init(frame: CGRect) {
self.init(customSearchBar: UISearchBar())
self.frame = frame
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
searchBar.frame = bounds
}
}
class MyViewController: UIViewController {
func setupNavigationBar() {
let searchBar = UISearchBar()
let searchBarContainer = SearchBarContainerView(customSearchBar: searchBar)
searchBarContainer.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: 44)
navigationItem.titleView = searchBarContainer
}
}
This work correctly but have issue when you change it to landscape from portrait.
– Malav Soni
Oct 13 '17 at 13:23
Great solution if you're not using auto layout. So far, this seems to work for landscape mode too, in the simulator.
– Ryan H.
Oct 14 '17 at 22:10
I have posted a solution that fixes orientation change issues.
– de.
Nov 10 '17 at 23:16
This answer solved all problems. zgjie's answer is great but search bar frame still changed after tap the textfield.
– Charlie Hung
Nov 23 '17 at 3:08
This should be accepted answer
– Prashant Tukadiya
Apr 10 at 11:12
I got black line under NavigationBar with SearchBar in iOS 11 in two cases:
when i pushed ViewControllers from ViewController with UISearchBar
when i dismissed ViewController with UISearchBar with "drag right to dismiss"
My solution was: adding this code to my ViewController with UISearchBar:
-(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
[self.navigationController.view setNeedsLayout]; // force update layout
[self.navigationController.view layoutIfNeeded]; // to fix height of the navigation bar
}
Swift 4 Update
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.view.setNeedsLayout() // force update layout
navigationController?.view.layoutIfNeeded() // to fix height of the navigation bar
}
Exactly what I was looking for, thank you!
– ArturT
Jan 29 at 7:22
This technique worked with 1 small adjustment - according to Apple's documentation, you should not call
layoutSubviews directly
. Instead setNeedsLayout
and layoutIfNeeded
should be called. developer.apple.com/documentation/uikit/uiview/…– KoCMoHaBTa
Mar 3 at 9:06
layoutSubviews directly
setNeedsLayout
layoutIfNeeded
Finally, 2 hours of searching for the solution payed off. Thank you! I had to think about it myself...
– Soberman
May 6 at 0:13
try this code on "ACKNOWLEDGEMENTS" view controller
in viewDidLoad
self.extendedLayoutIncludesOpaqueBars = true
Thanks! This is the solution I ended up going with, since I'm not using translucent bars.
– mmh02
Jan 2 at 23:00
This should be the right solution ! Thank you :)
– Grifas
Feb 5 at 15:12
Indeed this should be the answer. But I would like to add that place this piece of code in both the controller that over and below the search bar controller.
– Amrit Sidhu
Feb 10 at 10:32
That worked for me, but a had to add navigationController?.view.layoutSubviews() in viewWillDisappear.
– douglasd3
Feb 27 at 14:58
In Objective-C
if (@available(iOS 11.0, *)) {
[self.searchBar.heightAnchor constraintLessThanOrEqualToConstant: 44].active = YES;
}
I found Mai Mai's solution to be the only one that's really usable.
However it's still not perfect:
When rotating the device, the search bar is not properly resized and remains in the smaller dimension.
I have found a fix for that. Here is my code in Objective C with the relevant parts annotated:
// improvements in the search bar wrapper
@interface SearchBarWrapper : UIView
@property (nonatomic, strong) UISearchBar *searchBar;
- (instancetype)initWithSearchBar:(UISearchBar *)searchBar;
@end
@implementation SearchBarWrapper
- (instancetype)initWithSearchBar:(UISearchBar *)searchBar {
// setting width to a large value fixes stretch-on-rotation
self = [super initWithFrame:CGRectMake(0, 0, 4000, 44)];
if (self) {
self.searchBar = searchBar;
[self addSubview:searchBar];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
self.searchBar.frame = self.bounds;
}
// fixes width some cases of resizing while search is active
- (CGSize)sizeThatFits:(CGSize)size {
return size;
}
@end
// then use it in your VC
@implementation MyViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.titleView = [[SearchBarWrapper alloc] initWithSearchBar:self.searchController.searchBar];
}
@end
Now there is still one case left that I haven't figured out yet. To reproduce do the following:
- start in portrait
- activate search field
- rotate to landscape
- error: the bar doesn't resize
I couldn't use the solution of keeping the navBar at 44.
So it took me a day but finally, I found a solution that doesn't change the bar height and position the button in the middle of the bar. The issue is that the buttons are placed in a stack view which is configured as Horizontal stack view and therefore doesn't adjust to the height change.
This is done on init:
UIBarButtonItem *cancelButton;
if (@available(iOS 11.0, *)) {
// For iOS11 creating custom button to accomadate the change of navbar + search bar being 56 points
self.navBarCustomButton = [UIButton buttonWithType:UIButtonTypeCustom];
[self.navBarCustomButton setTitle:@"Cancel"];
[self.navBarCustomButton addTarget:self action:@selector(cancelButtonTapped) forControlEvents:UIControlEventTouchUpInside];
cancelButton = [[UIBarButtonItem alloc] initWithCustomView:self.navBarCustomButton];
} else {
cancelButton = [[UIBarButtonItem alloc] initWithTitle:MagicLocalizedString(@"button.cancel", @"Cancel")
style:UIBarButtonItemStylePlain
target:self
action:@selector(cancelButtonTapped)];
}
on viewWillApear (or anytime after the view was added to the navigation stack)
if (@available(iOS 11.0, *)) {
UIView *buttonsStackView = [navigationController.navigationBar subviewOfClass:[UIStackView class]];
if (buttonsStackView ) {
[buttonsStackView.centerYAnchor constraintEqualToAnchor:navigationController.navigationBar.centerYAnchor].active = YES;
[self.navBarCustomButton.heightAnchor constraintEqualToAnchor:buttonsStackView.heightAnchor];
}
}
And subviewOfClass is a category on UIView:
- (__kindof UIView *)subviewOfClass:(Class)targetClass {
// base case
if ([self isKindOfClass:targetClass]) {
return self;
}
// recursive
for (UIView *subview in self.subviews) {
UIView *dfsResult = [subview subviewOfClass:targetClass];
if (dfsResult) {
return dfsResult;
}
}
return nil;
}
In my case, bigger UINavigationBar's height wasn't a problem for me. I just needed to realign left and right bar button items. That's the solution i've come up with:
- (void)iOS11FixNavigationItemsVerticalAlignment
{
[self.navigationController.navigationBar layoutIfNeeded];
NSString * currSysVer = [[UIDevice currentDevice] systemVersion];
if ([currSysVer compare:@"11" options:NSNumericSearch] != NSOrderedAscending)
{
UIView * navigationBarContentView;
for (UIView * subview in [self.navigationController.navigationBar subviews])
{
if ([subview isKindOfClass:NSClassFromString(@"_UINavigationBarContentView")])
{
navigationBarContentView = subview;
break;
}
}
if (navigationBarContentView)
{
for (UIView * subview in [navigationBarContentView subviews])
{
if (![subview isKindOfClass:NSClassFromString(@"_UIButtonBarStackView")]) continue;
NSLayoutConstraint * topSpaceConstraint;
NSLayoutConstraint * bottomSpaceConstraint;
CGFloat topConstraintMultiplier = 1.0f;
CGFloat bottomConstraintMultiplier = 1.0f;
for (NSLayoutConstraint * constraint in navigationBarContentView.constraints)
{
if (constraint.firstItem == subview && constraint.firstAttribute == NSLayoutAttributeTop)
{
topSpaceConstraint = constraint;
break;
}
if (constraint.secondItem == subview && constraint.secondAttribute == NSLayoutAttributeTop)
{
topConstraintMultiplier = -1.0f;
topSpaceConstraint = constraint;
break;
}
}
for (NSLayoutConstraint * constraint in navigationBarContentView.constraints)
{
if (constraint.firstItem == subview && constraint.firstAttribute == NSLayoutAttributeBottom)
{
bottomSpaceConstraint = constraint;
break;
}
if (constraint.secondItem == subview && constraint.secondAttribute == NSLayoutAttributeBottom)
{
bottomConstraintMultiplier = -1.0f;
bottomSpaceConstraint = constraint;
break;
}
}
CGFloat contentViewHeight = navigationBarContentView.frame.size.height;
CGFloat subviewHeight = subview.frame.size.height;
topSpaceConstraint.constant = topConstraintMultiplier * (contentViewHeight - subviewHeight) / 2.0f;
bottomSpaceConstraint.constant = bottomConstraintMultiplier * (contentViewHeight - subviewHeight) / 2.0f;
}
}
}
}
Basically, we search for stack views that contain bar button items and then changing their's top and bottom constraints values. Yeah, it's a dirt hack, and won't recommend to use it if you can fix your issue in any other way.
I want to do the same thing as you, but I'll wait till a more elegant solution comes up. Thanks for sharing anyway.
– Michael Pirotte
Sep 29 '17 at 9:00
EDIT: The @zgjie answer is a better solution for this problem: https://stackoverflow.com/a/46356265/1713123
It seems this happens because in iOS 11 the default height value of SearchBar was changed to 56, instead 44 on previous iOS versions.
For now, I've applied this workaround, setting searchBar height back to 44:
let barFrame = searchController.searchBar.frame
searchController.searchBar.frame = CGRect(x: 0, y: 0, width: barFrame.width, height: 44)
Another solution could be use the new searchController property on navigationItem in iOS 11:
navigationItem.searchController = searchController
But this way da searchBar appears below navigation title.
Is the change documented somewhere? I couldn't find it here.
– Iulian Onofrei
Sep 27 '17 at 16:21
FYI, you can modify the frame without making a variable for it;
CGRect().insetBy(dx:dy:)
and CGRect().offsetBy(dx:dy:)
, in this case - searchController.searchBar.frame = searchController.searchBar.frame.insetBy(dx: 0, dy: -12)
– Oscar Apeland
Sep 28 '17 at 8:24
CGRect().insetBy(dx:dy:)
CGRect().offsetBy(dx:dy:)
searchController.searchBar.frame = searchController.searchBar.frame.insetBy(dx: 0, dy: -12)
This solution worked but when you click the search bar the height is reset.
– Gustavo_fringe
Oct 10 '17 at 20:04
I tried various things to get the size back to the original 44, but then the search bar always looks and behaves weird - like being to far stretched, y-offset and alike.
I found a nice solution here (via some other stackoverflow post):
https://github.com/DreamTravelingLight/searchBarDemo
Just derive your viewcontroller from the SearchViewController and include in your project the SearchViewController and WMSearchbar classes. Worked out of the box for me without any ugly if (iOS11) else... uglyness.
All solution didn't work for me so before I pushed view controller I did:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.navigationItem.titleView = UIView()
}
And to make search bar present when going back:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationItem.titleView = UISearchBar()
}
This removed the black view in the new viewcontroller but when returning to this viewcontroller(the one that contains searchBar), the rootView actually moved up a little bit.
– code4latte
Dec 21 '17 at 1:38
In my case, I have to decrease the textField's height 36pt -> 28pt.
So I tried to change the frame's height, layer's height. But the ways didn't work.
Finally, I found a solution that's the mask.
I think, It's not a good way but it works.
let textField = searchBar.value(forKey: "searchField") as? UITextField
textField?.font = UIFont.systemFont(ofSize: 14.0, weight: .regular)
textField?.textColor = #colorLiteral(red: 0.1960784314, green: 0.1960784314, blue: 0.1960784314, alpha: 1)
textField?.textAlignment = .left
if #available(iOS 11, *) {
let radius: CGFloat = 5.0
let magnifyIconWidth: CGFloat = 16.0
let inset = UIEdgeInsets(top: 4.0, left: 0, bottom: 4.0, right: 0)
let path = CGMutablePath()
path.addArc(center: CGPoint(x: searchBar.bounds.size.width - radius - inset.right - magnifyIconWidth, y: inset.top + radius), radius: radius, startAngle: .pi * 3.0/2.0, endAngle: .pi*2.0, clockwise: false) // Right top
path.addArc(center: CGPoint(x: searchBar.bounds.size.width - radius - inset.right - magnifyIconWidth, y: searchBar.bounds.size.height - radius - inset.bottom), radius: radius, startAngle: 0, endAngle: .pi/2.0, clockwise: false) // Right Bottom
path.addArc(center: CGPoint(x: inset.left + radius, y: searchBar.bounds.size.height - radius - inset.bottom), radius: radius, startAngle: .pi/2.0, endAngle: .pi, clockwise: false) // Left Bottom
path.addArc(center: CGPoint(x: inset.left + radius, y: inset.top + radius), radius: radius, startAngle: .pi, endAngle: .pi * 3.0/2.0, clockwise: false) // Left top
let maskLayer = CAShapeLayer()
maskLayer.path = path
maskLayer.fillRule = kCAFillRuleEvenOdd
textField?.layer.mask = maskLayer
}
You can change the insets, if you want to change the textField's frame.
I fixed this by added the constraint to viewDidAppear on the map view controller where the search bar is embedded
public override func viewDidAppear(_ animated: Bool) {
if #available(iOS 11.0, *) {
resultSearchController?.searchBar.heightAnchor.constraint(equalToConstant: 44).isActive = true
// searchBar.heightAnchor.constraint(equalToConstant: 44).isActive = true
}
}
All you have to do is to subclass UISearchBar and override "intrinsicContentSize":
@implementation CJSearchBar
-(CGSize)intrinsicContentSize{
CGSize s = [super intrinsicContentSize];
s.height = 44;
return s;
}
@end
//
// Created by Sang Nguyen on 10/23/17.
// Copyright © 2017 Sang. All rights reserved.
//
import Foundation
import UIKit
class CustomSearchBarView: UISearchBar {
final let SearchBarHeight: CGFloat = 44
final let SearchBarPaddingTop: CGFloat = 8
override open func awakeFromNib() {
super.awakeFromNib()
self.setupUI()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.setupUI()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// fatalError("init(coder:) has not been implemented")
}
func findTextfield()-> UITextField?{
for view in self.subviews {
if view is UITextField {
return view as? UITextField
} else {
for textfield in view.subviews {
if textfield is UITextField {
return textfield as? UITextField
}
}
}
}
return nil;
}
func setupUI(){
if #available(iOS 11.0, *) {
self.translatesAutoresizingMaskIntoConstraints = false
self.heightAnchor.constraint(equalToConstant: SearchBarHeight).isActive = true
}
}
override func layoutSubviews() {
super.layoutSubviews()
if #available(iOS 11.0, *) {
if let textfield = self.findTextfield() {
textfield.frame = CGRect(x: textfield.frame.origin.x, y: SearchBarPaddingTop, width: textfield.frame.width, height: SearchBarHeight - SearchBarPaddingTop * 2)`enter code here`
return
}
}
}
}
By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.
Possible duplicate of iOS 11 SearchBar in NavigationBar
– Jakub Truhlář
Nov 1 '17 at 15:15