85
votes

I used the Interface Builder to create a table view, to which I added the library's Search Bar and Search Display Controller to add search functionality. However, IB set it up so that the bar is visible at the top of the screen when the view is first displayed.

I'd like to know how to have the search bar be hidden by default but still scrollable with the table view (see Apple's Mail application for an example). I've tried calling scrollRectToVisible:animated: in viewDidLoad to scroll the table view down, but to no avail. What's the preferred way of hiding the search bar by default?

22

22 Answers

116
votes

First make sure, to add the UISearchBar to the tableHeaderView of the UITableView so that it gets scrolled with the table's content and isn't fixed to the top of the view.

The searchbar isn't counted as a row in the tableview, so if you scroll the top of the tableview to the first row, it 'hides' the searchbar:

[yourTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:NO];

or in Swift:

yourTableView.scrollToRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 0), atScrollPosition: UITableViewScrollPosition.Top, animated: false)

Make sure to not scroll the tableview before it contains data (scrollToRowAtIndexPath will raise an exception if the given indexPath does not point to a valid row (i.e. if the tableview is empty)).

45
votes

Dont' add the UISearchBar as a subview of the UITableView, this isn't necessary.

The UITableView has a tableHeaderView property that is perfect for this:

- (void) viewDidLoad {
    [super viewDidLoad]; 
    self.searchBar = [[[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, 320, 44)] autorelease];
    self.searchBar.showsCancelButton = YES;    
    self.searchBar.delegate = self;
    self.tableView.tableHeaderView = self.searchBar;     
}

If you don't want to see it by default:

- (void) viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [self.tableView setContentOffset:CGPointMake(0, 44)];
}

I also get the cancel button to hide it again.... (and remove the keyboard)

- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
    [self.tableView setContentOffset:CGPointMake(0, 44) animated:YES];
    [self.searchBar resignFirstResponder];
}
25
votes

The contentOffset was noted in the comments by Tim and Joe D'Andrea, but this is a bit expanded by adding an animation to hiding the search bar. I noticed it is a bit unexpected for the search bar to just disappear.

A little update for those who want to target iOS 4.0 and above, try this:

[UIView animateWithDuration:0.4 animations:^{
    self.tableView.contentOffset = CGPointMake(0, self.searchDisplayController.searchBar.frame.size.height);
 } completion:nil];

Previous answer:

[UIView beginAnimations:@"hidesearchbar" context:nil];
[UIView setAnimationDuration:0.4];
[UIView setAnimationBeginsFromCurrentState:YES];

self.tableView.contentOffset = CGPointMake(0, self.searchDisplayController.searchBar.frame.size.height);

[UIView commitAnimations];

8
votes

There are many answers to this solution, however none worked very well for me.

This works perfectly. No animation, and is done upon -viewDidLoad

 - (void)viewDidLoad {
     [super viewDidLoad];
     self.tableView.contentOffset = CGPointMake(0,  self.searchBar.frame.size.height - self.tableView.contentOffset.y);
 }

Note:

Code assumes you have the searchBar @property which is linked to the search bar. If not, you can use self.searchDisplayController.searchBar instead

7
votes

For Swift 3+

I did this:

Declare this var:

    var searchController = UISearchController()

And in the viewDidLoad() method

    searchController = UISearchController(searchResultsController: nil)
    searchController.searchResultsUpdater = self
    searchController.hidesNavigationBarDuringPresentation = false
    searchController.dimsBackgroundDuringPresentation = true
    searchController.searchBar.placeholder = NSLocalizedString("Search", comment: "")
    definesPresentationContext = true
    tableView.tableHeaderView = searchController.searchBar

    tableView.contentOffset = CGPoint(x: 0, y: searchController.searchBar.frame.size.height)
5
votes

My goal was to hide search bar added to tableHeaderView of plain TableView's style in storyboard when the view controller is loaded and show the bar when user scroll down at the top of UITableView. So, I'm using Swift and has added extension:

extension UITableView {
    func hideSearchBar() {
        if let bar = self.tableHeaderView as? UISearchBar {
            let height = CGRectGetHeight(bar.frame)
            let offset = self.contentOffset.y
            if offset < height {
                self.contentOffset = CGPointMake(0, height)
            }
        }
    }
}

in viewDidLoad() just call:

self.tableView.hideSearchBar()
5
votes

All existing solutions don't work for me on iOS 8 when there are not enough rows to fill the tableView since iOS will adjust the inset automatically in this situation. (Existing answers are good when there are enough rows though)

After wasting like 6 hours on this issue, I finally got this solution.

In short, you need to insert empty cells into the tableView if there are not enough cells, so the content size of the tableView is big enough that iOS won't adjust the inset for you.

Here is how I did it in Swift:

1.) declare a variable minimumCellNum as a class property

var minimumCellNum: Int?

2.) calculate minimumCellNum and set tableView.contentOffset in viewWillAppear

let screenHeight = Int(UIScreen.mainScreen().bounds.height)

// 101 = Height of Status Bar(20) + Height of Navigation Bar(44) + Height of Tab Bar(49)
// you may need to subtract the height of other custom views from the screenHeight. For example, the height of your section headers.
self.minimumCellNum = (screenHeight - 103 - heightOfOtherCustomView) / heightOfYourCell
self.tableView.contentOffset = CGPointMake(0, 44)

3.) in tableView(tableView: UITableView, numberOfRowsInSection section: Int))

let numOfYourRows = YOUR LOGIC
if numOfYourRows > minimumCellNum {
    return numOfYourRows
} else {
    return minimumCellNum!
}

4.) Register an empty cell, whose selection attribute is None, on the storyboard and in tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath)

if indexPath.row < numOfYourRows {
    return YOUR CUSTOM CELL
} else {
    let cell = tableView.dequeueReusableCellWithIdentifier("EmptyCell", forIndexPath: indexPath) as! UITableViewCell
    return cell
}

5.) in tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)

if tableView == self.tableView {
    if numOfYourRows < (indexPath.row + 1) {
        return
    }
    YOUR LOGIC OF SELECTING A CELL
}

This is not a perfect solution, but it's the only workaround that really works for me on iOS 8. I'd like to know if there is a neater solution.

4
votes

None of the above worked for me, so just in case if someone will have the same issue.

I have searchBar set as tableHeaderView on a grouped table on iOS7. In viewDidLoad I have tableView.contentOffset = CGPointMake(0, 44); to keep searchBar initially "hidden". When user scrolls down searchBar is dsiplayed

Goal: hide searchBar on cancel button tap

In searchBarCancelButtonClicked(searchBar: UISearchBar!)

This did not work: [yourTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:NO]; tableView was scrolling too much up, resulting in row 0 going behind toolBar.

This did not work: tableView.ContentOffset = CGPointMake(0, 44) - nothing happens

This did not work: tableView.ContentOffset = CGPointMake(0, searchBar.frame.size.height) - nothing happens

This did not work: tableView.setContentOffset(CGPointMake(0, 44), animated: true); Again tableView was scrolling too much up, resulting in row 0 going behind toolBar.

This worked partially: tableView.setContentOffset(CGPointMake(0, 0) but titleForHeaderInSection was going behind toolBar

THIS WORKED: tableView.setContentOffset(CGPointMake(0, -20)

Seems not logical but only -20 moved it up to correct position

4
votes

Content offset is the way to go, but it doesn't work 100% of the time.

It doesn't work if are setting estimatedRowHeight to a fixed number. I don't know the work around yet. I've created a project showing the issue(s) and I've created a radar with Apple.

Check out the project if you want an example in swift of how to set it all up.

3
votes

Note that the accepted answer will crash if there is no data in the tableview. And adding it to viewDidLoad may not work under certain circumstances.

[yourTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:NO]; // crashes if no rows/data

Therefore, instead add this line of code:

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    [yourTableView setContentOffset:CGPointMake(0, self.searchDisplayController.searchBar.frame.size.height)];
}
2
votes

Honestly, none of the answers really solved the issue (for me). If your table view has no rows OR the row height is different, these answers don't suffice.

The only thing that works:

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    self.tableView.contentOffset = (CGPoint){.y =  self.tableView.contentOffset.y + self.searchController.searchBar.frame.size.height};
}
2
votes

The scrollToRowAtIndexPath method causes a crash if there are zero rows in the table.

UITableView is just a scroll view though, so you can simply change the content offset.

This checks that you have something as your tableHeaderView, and assuming you do scrolls to hide it

    if let searchHeight = tableView.tableHeaderView?.frame.height {
        tableView.setContentOffset(CGPoint.init(x: 0, y:searchHeight ), animated: false)
    }
1
votes

I find that the "Notes" app is unable to search items when the tableView is blank. So I think it's just using -(void)addSubview:(UIView *)view to add a blank table at first. When you add an item to its data source array, it will run another peice of code to load tableView.

So my solution is:

if([self.arrayList count] > 0)
{
     [self.tableView reloadData];     //use jdandrea's code to scroll view when cell==nil in cellForRowAtIndexPath
     self.tableView.scrollEnabled = YES;
}
else
{
     tableBlank = [[UITableView alloc]initWithFrame:CGRectMake(0,0,320,416) style:UITableViewStylePlain] autorelease];
     tableBlank.delegate = self;
     tableBlank.dataSource = self;
     [self.view addSubview:tableBlank];
}

Hope this helps :-)

1
votes

Change the bounds of the tableView, this can be done inside viewDidLoad plus you don't have to worry about if the table is empty or not (to avoid the exception).

CGRect newBounds = self.tableView.bounds;
newBounds.origin.y = newBounds.origin.y + self.searchBar.bounds.size.height;
self.tableView.bounds = newBounds;

Once the user has scrolled down the table the search bar will appear and behave normally

1
votes

The other answers to this question either didn't work at all for me, had odd behavior when the tableView had 0 rows, or messed up the scroll position when going back and forth between parent and nested line items. This worked for me (the goal being to hide the UISearchBar when on load):

public override func viewWillAppear(animated: Bool) {        
    if self.tableView.contentOffset.y == 0 {
        self.tableView.contentOffset = CGPoint(x: 0.0, y: self.searchBar.frame.size.height) 
    }
}

Explanation
Anytime the tableView will appear, this code checks to see if the UISearchBar is visible (meaning the y position of the contentOffset, aka scroll position, is 0). If it is, it simply scrolls down the height of the searchBar. If the searchBar is not visible (think a larger list of items in tableView), then it won't try and scroll the tableView, as this would mess up the positioning when you return from a nested line item.

Side Note
I'd recommend putting this code in a separate method to ensure single responsibility and give you the reusability to call it anytime in your code.

1
votes

If your table will always have at least one row, just scroll to the first row of the table and the search bar will be hidden automatically.

let firstIndexPath = NSIndexPath(forRow: 0, inSection: 0)

self.tableView.selectRowAtIndexPath(firstIndexPath, animated: false, scrollPosition: .Top)

If you put the above code on viewDidLoad, it will throw an error because the tableView hasn't loaded yet, so you have to put it in viewDidAppear because by this point the tableView has already loaded.

If you put it on viewDidAppear, everytime you open the tableView it will scroll to the top.

Maybe you don't want this behaviour if the tableView remains open, like when it is a UITabBar View Controller or when you do a segue and then come back. If you just want it to scroll to the top on the initial load, you can create a variable to check if it is an initial load so that it scrolls to the top just once.

Define first a variable called isInitialLoad in the view controller class and set it equal to "true":

var isInitialLoad = true

Then check if isInitialLoad is true on viewDidAppear and if it is true, scroll to the top and set the isInitialLoad variable to false:

if isInitialLoad {
            let firstIndexPath = NSIndexPath(forRow: 0, inSection: 0)
            self.tableView.selectRowAtIndexPath(firstIndexPath, animated: false, scrollPosition: .Top)

            isInitialLoad = false
        }
1
votes

Below code is working for me

self.tableView.contentOffset = CGPointMake(0.0, 44.0);
1
votes

I tried setting the content offset and scrollToIndexpath but nothing worked satisfactorily. I removed the setting of tableHeaderView as searchBar from viewDidLoad and set it in scrollview delegate as below

override func scrollViewWillBeginDragging(scrollView: UIScrollView) {
    if scrollView.contentOffset.y < 0 && tableView.tableHeaderView == nil {
        tableView.tableHeaderView = searchController.searchBar
        tableView.setContentOffset(CGPointMake(0, scrollView.contentOffset.y + searchController.searchBar.frame.size.height), animated: false)
    }
}

This worked like a charm.The content offset setting is for smooth scrolling purpose

0
votes

Don't forget to take status bar and navigation bar into consideration. My table view controller is embedded in a navigation controller, in viewDidAppear:

tableView.contentOffset = CGPointMake(0, -20) // -20 = 44 (search bar height) - 20 (status bar height) - 44 (navigation bar height)

worked for me.

0
votes

For Swift:

Just make tableview content y offset, which should be like height of searchbar.

self.tableView.contentOffset = CGPointMake(0, self.searchController.searchBar.frame.size.height)
0
votes

Swift 3

works with empty table too!

In my environment I load custom cells by xib file and the rowHeight is automatic set.

    override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        self.tableView.contentOffset = CGPoint(x: 0, y: self.tableView.contentOffset.y + self.searchController.searchBar.bounds.height)
}
0
votes

I had similar problem. And only way i found to resolve this problem was to use key value observer on UITableView contentOffset property.

After some debugging i realized that problem appear just if table view content size is smaller than tableview. I start KVO when on viewWillAppear. And dispatch stop KVO 0.3 seconds after view appear. Every time contentOffset became 0.0, KVO react and set contentOffset on search bar height. I stop KVO asynchronous after 0.3 seconds because view layout will reset contentOffset even after view appear.

- (void)viewWillAppear:(BOOL)animated {
  [super viewWillAppear:animated];
  [self.tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
}

-(void)viewDidAppear:(BOOL)animated{
   __weak typeof (self) weakSelf = self;
   dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [weakSelf.tableView removeObserver:self forKeyPath:@"contentOffset"];});
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    if ([keyPath isEqualToString:@"contentOffset"] && self.tableView.contentOffset.y == 0.0) {
       self.tableView.contentOffset = CGPointMake(0, CGRectGetHeight(self.tableView.tableHeaderView.frame));
    }
}

-(void)dealloc {
    [self.tableView removeObserver:self forKeyPath:@"contentOffset"];
}