Search code examples
iosuisearchcontroller

UISearchController unexpected paint of UITableView in status bar since IOS 13


I have a very basic UITableController within a very basic UINavigationBar using a very basic UISearchController ( but hidesNavigationBarDuringPresentation = YES ). The issue I have is that if the search is active the UITableView scroll is visible in the statusbar. Note that this behavior is new since IOS13 , before due to the opaque search bar this didn't happen. It looks more like an IOS bug to me than the expected behavior. Any suggestions how to fix this ? Ideally I'd like to have the same appearance like in IOS12 and before.

IOS 13, scrolling visible in statusbarIOS 13, scrolling visible in statusbar

IOS 12, search bar half way opaque: no problem IOS 12, search bar opaque: no problem
(source: generomobile.de)

#import <UIKit/UIKit.h>
@interface MyAppDelegate : UIResponder <UIApplicationDelegate,UITableViewDelegate,UITableViewDataSource,UISearchResultsUpdating>
{
  UIWindow* _win;
  UINavigationController* _nav;
  UITableViewController* _tc;
  NSMutableArray* _model;
  NSArray* _filteredModel;
  UISearchController* _search;
  UILabel* _label;
}
@end
@implementation MyAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  _win=[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
  UIViewController* c=[[UIViewController alloc] init];
  UIView* v=c.view;
  v.backgroundColor=UIColor.whiteColor;
  _label=[[UILabel alloc] initWithFrame:CGRectMake(10, 100, 200, 20)];
  _model=[NSMutableArray array];
  _label.text=@"Tap on 'Show Table'";
  [v addSubview:_label];
  for (int i=0;i<200;i++) {
    _model[i]=[NSString stringWithFormat:@"Item %d",i];
  }
  _nav=[[UINavigationController alloc] initWithRootViewController:c];
  c.navigationItem.title=@"A title";
  UIBarButtonItem* b=[[UIBarButtonItem alloc] initWithTitle:@"Show Table" style:UIBarButtonItemStylePlain target:self action:@selector(showTable:)];
  c.navigationItem.rightBarButtonItem=b;
  _tc=[[UITableViewController alloc] initWithStyle:UITableViewStylePlain];
  _tc.definesPresentationContext=YES; //have to do it for other reasons
  _search=[[UISearchController alloc] initWithSearchResultsController:nil];
  _search.obscuresBackgroundDuringPresentation=NO; //needed to be able to tap in results
  _search.hidesNavigationBarDuringPresentation=YES; //have to do it for other reasons
  _search.searchResultsUpdater=self;
  UITableView* tv=_tc.tableView;
  tv.delegate=self;
  tv.dataSource=self;
  tv.tableHeaderView=_search.searchBar;
  _win.rootViewController=_nav;
  [_win makeKeyAndVisible];
  return YES;
}
-(void)showTable:(UIBarButtonItem*)b {
  [_nav pushViewController:_tc animated:YES];
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
  _label.text=[NSString stringWithFormat:@"You selected row:%ld",(long)indexPath.row];
  [_nav popViewControllerAnimated:TRUE];
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
  return _search.active?_filteredModel.count:_model.count;
}
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  static NSString* reuse=@"whatever";
  UITableViewCell* cell=[tableView dequeueReusableCellWithIdentifier:reuse];
  if (cell==nil) {
    cell=[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:reuse];
  }
  NSInteger row=indexPath.row;
  cell.textLabel.text=_search.active?_filteredModel[row]:_model[row];
  return cell;
}
-(void)updateSearchResultsForSearchController:(UISearchController *)searchController {
  NSString* searchText=searchController.searchBar.text;
  if (searchText.length==0) {
    _filteredModel=_model;
  } else {
    NSPredicate *predicate = [NSPredicate predicateWithFormat:
                                              @"SELF contains[c] %@",searchText];
    _filteredModel=[_model filteredArrayUsingPredicate:predicate];
  }
  [_tc.tableView reloadData];
}
@end
int main(int argc, char * argv[]) {
  @autoreleasepool {
      return UIApplicationMain(argc, argv, nil, NSStringFromClass([MyAppDelegate class]));
  }
}

Solution

  • The reason for this (mis)behavior is that on IOS13 the UISearchBarBackground view does not cover the status bar anymore. It did cover it in previous IOS versions.

    IOS12 UISearchBarBackground covering the whole upper areacovers the whole upper area

    IOS13 UISearchBarBackground not covering the statusbarcovers only the status bar itself
    (source: generomobile.de)

    The solution I came up with without touching any other ViewController hierarchies in my code was a pretty nasty hack, extending the frame of the UISearchBarBackground to match with the state in pre IOS13 versions.

    -(void)updateSearchResultsForSearchController:(UISearchController *)searchController {
      NSString* searchText=searchController.searchBar.text;
      if (@available(iOS 13.0, *)) {
        if (searchController.active) {
          //The intrinsic UISearchBar code seems to adjust the
          //background view of UISearchBar later on thats why we
          //need to delay
          [self performSelector:@selector(adjustBg) withObject:nil afterDelay:0.1];
        }
      }
      if (searchText.length==0) {
        _filteredModel=_model;
      } else {
        NSPredicate *predicate = [NSPredicate predicateWithFormat:
                                                  @"SELF contains[c] %@",searchText];
        _filteredModel=[_model filteredArrayUsingPredicate:predicate];
      }
      [_tc.tableView reloadData];
    }
    CGPoint pointInScreen(UIView* v,CGPoint pt)
    {
      CGPoint ptWin=[v.superview convertPoint:pt toView:nil];
      CGPoint ptScreen=[v.window convertPoint:ptWin toWindow:nil];
      return ptScreen;
    }
    -(void)adjustBg
    {
      //ugly: we try to adjust the background view of the UISearchBar
      //to the top of the screen (like it was in pre IOS13 times)
      if (!_search.active) {
        return;
      }
      UISearchBar* bar=_search.searchBar;
      if (bar.subviews.count==0) { return; }
      UIView* first=bar.subviews[0];
      if (first.class!=UIView.class || first.subviews.count==0) { return;}
      UIView* bg=first.subviews[0];
      if ([bg isKindOfClass:NSClassFromString(@"UISearchBarBackground")]) {
        CGSize size=bg.frame.size;
        CGPoint orig=bg.frame.origin;
        CGPoint p=pointInScreen(bg,orig);
        CGPoint p2=pointInScreen(bg, CGPointMake(0, orig.y+size.height));
        if (p.y>0) { //not top of screen
          bg.frame=CGRectMake(orig.x,-p.y,size.width,p2.y);
        }
      }
    }
    

    It's really not the solution I was looking for , but as long there is no better one, I need to maintain that (and post it here in case someone else stumbles across that)