UISearchBar with tableHeaderView layout problem under iOS 13

I have a UITableViewController in Objective-C that is supposed to launch without a search bar, but a UISearchBar is added on a certain button press. Whenever the search bar is added, I also add a tableHeaderView to the UITable that works in conjunction with the search bar. It is all working well, except in iOS 13 where this happens:


When the tableHeaderView (which was initially nil) is set to the appropriate view, that view appears and covers the top line of the table under it. If there are 3 items in the table, we only see 2 of them. If I try to scroll down the table, I can briefly expose that top table entry, but I cannot tap on it because it goes right back under the tableHeaderView when I let go of the scroll. But here is the strange thing: Under these conditions, with the tableHeaderView covering the top table entry, if I rotate between landscape and portrait, the re-layout that happens fixes the layout. After the rotation, all the items in the table are visible. Somehow the table has learned how to lay itself out to avoid the tableHeaderView. But if I cancel the search bar (and hence the tableHeaderView by setting it to nil) and then call for the search bar again, the problem comes back.


So what I would like to do is to force whatever recalculations were made in the rotation whenever the search bar is called for. Is there any way to force a view unload and view reload at such times? (or whatever happens in a rotation.)


Here is how the search bar is launched:

- (void)didPressSearch {
  if(self.searchController.isActive == NO)
  self.tableView.tableHeaderView = self.searchController.searchBar;
  [self.searchController.searchBar becomeFirstResponder];
}


Here is how the search bar is cancelled:


- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{
  self.tableView.tableHeaderView = nil;
}
Answered by tunelabguy in 401487022

This solution is not perfect, but after trying everything I could think of and everything I could find on-line, this is the closest I could come to the desired behavior: (for simpliciy, I am showing only what the code is when running under iOS 13+)


- (void)viewDidLoad
{
  [super viewDidLoad];
  UISearchController *sc = [[UISearchController alloc] initWithSearchResultsController:nil];
  self.searchController = sc;
  sc.searchResultsUpdater = self;
  UISearchBar *sb = sc.searchBar;
  sb.delegate = self;
  sb.scopeButtonTitles = @[@"Starting with...", @"Containing..."];
  sc.dimsBackgroundDuringPresentation = NO;
  self.navigationItem.searchController = sc;
  self.definesPresentationContext = YES;
  [sb sizeToFit];
}

- (void)didPressSearch {
  [self.searchController.searchBar becomeFirstResponder];
}

- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{
     //..nothing needed
}


With this code the operation is acceptable, but the search bar is always shown (albeit without the scope buttons). The scope buttons appear when the search bar becomes the first responder and they disappear when the Cancel button is pressed. But it would be nice if the whole search bar would disappear when the Cancel button is pressed. I am marking this thread closed because the solution is acceptable, even if it is not exactly what I wanted.

Correction to my original question: I incorrectly stated that the tableHeaderView was some separate view that worked in conjunction with the UISearchBar. In fact the tableHeaderView is the whole search bar. The part that I thought was a separate view was just the scope buttons, which are part of UISearchBar. So to correctly state the problem, the scope buttons cover the top entry in the table, until the display rotates, and then all entries in the table are visible.

Header view height seems to be the problem:

https://stackoverflow.com/questions/36586179/uisearchbar-scope-buttons-in-uitableview-header-hide-the-first-row-of-the-table


If that solves, thanks to tell what you had to change exactly.

Upon closer examination of the preferred behavior (as in iOS 12 and earlier) my existing code does this:

Before invoking the search function, the navigation bar is visible and directly under that starts my table view. When the search function is invoked (by pressing on a button) the navigation bar animates up and off the screen. The status bar remains throughout (in portrait only), but in place of the navigation bar is now my search field where text is to be entered, and the Cancel button. Immediately below that are two scope buttons that i defined. Immediately below that starts the table view. This is ideal, and I would like to maintain this behavior under iOS 13.


That same code under iOS 13 does this:

Before invoking the search function, the display is the same as under iOS 12 and earlier. When pressing the search button, the navigation bar animates up and off the screen just as before, but now the search field does not replace it. Instead the area once occupied by the navigation bar is now just blank wasted space. The search field and the scope buttons appear as before, but located so that the scope buttons now overlay the top item in the tableview. After rotating to landscape and back, the search field is now located up where the navigation bar used to be and the scope buttons no longer cover the top item in the table view. That is, everything looks exactly as it did under iOS 12 - ideal. So it looks like the table view is being sized to make room for the search bar assuming the search bar is located up where the navigation bar used to be, but isn't (until the screen is rotated).


As for the solutions suggested in the Stack Overflow article, this one:

searchController.hidesNavigationBarDuringPresentation = NO;
searchController.definesPresentationContext = NO;

inserted in viewDidLoad fixes the overlay problem, but it leaves the search field partially obscured by the navigation bar and there is now about half a row too much room between the scope buttons and the first table view entry. Plus I don't want to keep the navigation bar during the search. So that one is not acceptable.


As for the one that overrides search controller delegate methods to resize the table view header height, that one made no discernable difference. The top entry was still hidden and the navigation bar space was still blank. Screen rotation still fixed things up.


I am currently looking into an alternate method available only for iOS 11+ which does not use the tableHeaderView at all:


self.navigationItem.searchController = self.searchController;

instead of

self.tableView.tableHeaderView = self.searchController.searchBar;

which I had been using. I will report back here with an update if I get it working properly.


Edit: After trying the alternate approach of using the searchController property of the navigationItem of the UITableViewController I was unable to get it to work at all. I think one of the main problems is that all the examples I have seen are for a search bar that is there from the beginning. My app requires a search bar that appears and disappears on demand. At one point I had the new method working if activated the search bar twice. It didn't appear the first try. Then it appeared fine, with the scope buttons and everything. But then if I cancelled it and tried to invoke it again, it would appear without the scope buttons. This is an ongoing effort.

I am getting close with this new method, but there are still some strange behaviors I have not been able to correct, so I though I would just lay it all out. First, here is the behavior now: When press the search button (a bottom toolbar button I use to launch the search function), the search bar appears properly now, up over the nav bar the way I wanted it, with the proper scope buttons, and the table entries appear directly below with nothing obscured. Fine. If I tap the Cancel button of the search bar, the search bar disappears and everything goes back the way it was. Again, this is all fine so far. But now if I tap the search button again, something different happens. The search bar appears in half transparency on top of the top entry of the table. If I tap in the seach field (as if I were to enter text) the search bar moves up over the nav bar, which disappears, but the scope buttons are invisible. There is blank space for them, but nothing is visible. If I now tap the Cancell button for the search bar, the search bar goes away and eveything looks normal, but I can no longer select the top entry in the table. It is visible, but tapping on it does nothing. I can select any other entry, but not the first one. Apparently Cancelling the search bar destroys something that is never rebuilt. Anyway, here is the code:


- (void)viewDidLoad
{
  [super viewDidLoad];
  UISearchController *sc = [[UISearchController alloc] initWithSearchResultsController:nil];
  self.searchController = sc;
  sc.searchResultsUpdater = self;
  UISearchBar *sb = sc.searchBar;
  sb.delegate = self;
  sb.scopeButtonTitles = @[@"Starting with...", @"Containing..."];
  sc.dimsBackgroundDuringPresentation = NO;
  if (@available(iOS 11.0, *)) {
       self.navigationItem.searchController = sc;
  }

  self.definesPresentationContext = YES;
  [sb sizeToFit];
}


When my "Search" button is pressed, this code is called:

- (void)didPressSearch {
  if(self.searchController.isActive == NO)
  {
       if (@available(iOS 11.0, *)) {
       self.navigationItem.searchController = self.searchController;
       } else {
       self.tableView.tableHeaderView = self.searchController.searchBar;
       }
  }
  [self.searchController.searchBar becomeFirstResponder];
}


When the Cancel button is the search bar is pressed, this code is called:

- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{
  if (@available(iOS 11.0, *)) {
      self.navigationItem.searchController = nil;
  } else {
       self.tableView.tableHeaderView = nil;
  }
}


Here is the relevant portion of the class interface

@interface LFileExplorer : FileExplorer <UISearchBarDelegate, UISearchResultsUpdating>{
}
@property (strong, nonatomic) UISearchController *searchController;
@end

@interface FileExplorer : UITableViewController{
}
@end


I have omitted all the data handling code. It is working fine, and I don't think it has any affect on the appearance of the search bar, which is the only issue causing me problems.

Accepted Answer

This solution is not perfect, but after trying everything I could think of and everything I could find on-line, this is the closest I could come to the desired behavior: (for simpliciy, I am showing only what the code is when running under iOS 13+)


- (void)viewDidLoad
{
  [super viewDidLoad];
  UISearchController *sc = [[UISearchController alloc] initWithSearchResultsController:nil];
  self.searchController = sc;
  sc.searchResultsUpdater = self;
  UISearchBar *sb = sc.searchBar;
  sb.delegate = self;
  sb.scopeButtonTitles = @[@"Starting with...", @"Containing..."];
  sc.dimsBackgroundDuringPresentation = NO;
  self.navigationItem.searchController = sc;
  self.definesPresentationContext = YES;
  [sb sizeToFit];
}

- (void)didPressSearch {
  [self.searchController.searchBar becomeFirstResponder];
}

- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{
     //..nothing needed
}


With this code the operation is acceptable, but the search bar is always shown (albeit without the scope buttons). The scope buttons appear when the search bar becomes the first responder and they disappear when the Cancel button is pressed. But it would be nice if the whole search bar would disappear when the Cancel button is pressed. I am marking this thread closed because the solution is acceptable, even if it is not exactly what I wanted.

UISearchBar with tableHeaderView layout problem under iOS 13
 
 
Q