87

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?

TheNeil
  • 3,321
  • 2
  • 27
  • 52
Tim
  • 59,527
  • 19
  • 156
  • 165

22 Answers22

117

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)).

eonil
  • 83,476
  • 81
  • 317
  • 516
Zargony
  • 9,615
  • 3
  • 44
  • 44
  • 12
    Actually, I'm getting an issue where the table view won't scroll if there aren't enough rows to fill the screen - in a table with one or two rows, this code does nothing. Is that expected behavior? Can I get around it somehow? – Tim Jul 10 '09 at 19:54
  • Sounds like this is normally intended (display everything if fits the screen). The mail app however seems to be able to scroll away the searchbar even if no rows exist. Unfortunately I wasn't able to reproduce that in my own code. – Zargony Jul 14 '09 at 21:51
  • 19
    I found a way: instead of scrolling to an index path, change the content offset of the table's scroll view to a CGRect at 0.0, 44.0 – Tim Aug 03 '09 at 03:13
  • 108
    Tim, that's the way to go! I used self.tableView.contentOffset = CGPointMake(0, self.searchDisplayController.searchBar.frame.size.height); – Joe D'Andrea Aug 06 '09 at 01:44
  • 1
    Btw - when I was putting in this code after a call to [tableView reloadData] which caused my table to go from 2 rows to enough rows to allow normal scrolling, I found that using the contentOffset method caused a jerky showing and hiding of the search box while using the scrollToRowAtIndexPath method didn't have the same jerkiness. – Chris R Nov 22 '10 at 22:14
  • I've found that using animation is the key to the content offset approach. (See Andrei's answer.) – quickthyme Jun 05 '11 at 01:39
  • No need to add the search bar as a child of the UITableView, use the tableHeaderView property of the table. – bandejapaisa Mar 09 '12 at 10:57
  • You're right, bandejapaisa, it should be the tableHeaderView, not the tableview itself. That's also where the interface builder puts the view element that you drag onto the upper half of a tableview. – Zargony Mar 12 '12 at 14:48
  • With Joe D'Andrea's solution, you can call it even before any table data is loaded! – Hope4You Jan 29 '13 at 21:04
  • Didnt work for me.. where do you have to put that line? – Fabrizio Guespe Apr 08 '14 at 20:42
  • @Tim, `UITableViewScrollPositionNone` did the trick for me: It always shows the first row (including its header), but never the search bar, regardless if I have 1 or 100 rows. – hagi May 28 '14 at 13:53
  • 1
    This is the only ultimate way which works under ANY circumstances. Setting `contentOffset` with whatever value will break if you're employing complex view-controller layout. I don't recommend it. – eonil Nov 24 '14 at 03:39
  • If you have no section/row in your table, simply remove the search-bar from the table, and add it again when data becomes available. – eonil Nov 24 '14 at 03:43
  • Now is there any way to unhide the search bar - for example when the user taps a button on the navigation bar? – Imran Jul 27 '15 at 23:15
45

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];
}
Jingshao Chen
  • 3,405
  • 2
  • 26
  • 34
bandejapaisa
  • 26,576
  • 13
  • 94
  • 112
  • For me, this was the only solution that produced a hidden UISearchBar implementation that felt like it was in-line with the HIG. The other solutions where it's "stuck" to the top of the table view feel unnatural. – kurtzmarc Apr 19 '12 at 02:03
  • 1
    This solution worked the best. This should be the answer in my opinion. – darwindeeds Aug 06 '13 at 16:13
  • This solution is nice(+1). However, tableView doesn't hide the search bar properly in some cases(in my case: when the tableview controller is pushed back from another view controller). There is a question about this issue: http://stackoverflow.com/questions/15222186/uitableview-contentoffset-is-not-working-properly I think the provided answer of this question is not quite good though. @bandejapaisa, do you have any idea? – Brian May 15 '15 at 16:17
  • 3
    This worked well for me, though instead of hard-coding 44 throughout I suggest using self.tableView.tableHeaderView.frame.size.height in viewWillAppear to allow for changes Apple may make in the future to the search bar height. – John Stephen Jul 15 '15 at 20:58
  • putting this in `viewWillAppear` is a bad idea because it'll run everytime you navigate back to your view. This means your tableview will slowly inch down. Irritating bug! – Allison Jul 11 '19 at 03:58
25

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];

Sam Spencer
  • 8,492
  • 12
  • 76
  • 133
Alex
  • 7,432
  • 20
  • 75
  • 118
  • 9
    I've found that the animation is the key to this solution. Trying to set the content offset without the animation will appear to do nothing. Rather than use the animation methods (or blocks) in this sample however, I find that simply calling [self.tableView setContentOffset:CGPointMake(0,44) animated:TRUE] works well enough. – quickthyme Jun 05 '11 at 01:32
  • It seems to not work at all without the animation. @quickthyme has made the same observation. – Thilo Aug 03 '11 at 09:51
  • 5
    I've had success with this without needing the animations if you implement it in `viewDidAppear:` instead of `viewDidLoad`. – wbyoung Nov 08 '11 at 20:21
9

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

Matej
  • 9,548
  • 8
  • 49
  • 66
  • this is the only one that worked for me! , but it only does it un viewdidload... so first time its hiden, but then is not hiden anymore while de app is running. And i tried putting it in viewwillappear and viewdidappear but works wrong. Thanks! – Fabrizio Guespe Apr 08 '14 at 20:50
  • @fguespe that's strange.. The viewDidLoad should get called every time the view is instantiated. It never happened to me that the UISearchBar was visible after coming back from another view. – Matej Apr 08 '14 at 22:40
  • im using a tabbed application. Maybe that? – Fabrizio Guespe Apr 08 '14 at 23:53
  • @fguespe I see that could be the issue hmm. Unfortunately im not sure how you'd go about solving that if `-viewWillAppear` doesn't work.. Does it get called when the tab controller switches to the view? – Matej Apr 08 '14 at 23:59
  • viewwillappear and viewdidappear are called , but i tried with both and the view is like wrong offseted. It doesnt return the expected result. I mean, something does, but it does it bad. – Fabrizio Guespe Apr 09 '14 at 04:34
  • that would be nice but im banned to ask :( – Fabrizio Guespe Apr 11 '14 at 01:33
  • It doesn't return the correct result second time round because its taking into account the current y offset, which second time round in viewWillAppear (if you push new view controller and go back that is) will already be set to the height of the status bar. So first time around in viewWillAppear the calculation is 20 - 0 = 20. Push then pop new view controller, this time it's 20-20 = 0. Third time is 20 - 0 = 20 and so on... – Breeno Aug 02 '15 at 19:41
7

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)
Dasoga
  • 5,489
  • 4
  • 33
  • 40
5

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()
kbpontius
  • 3,867
  • 1
  • 30
  • 34
HotJard
  • 4,598
  • 2
  • 36
  • 36
5

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.

Brian
  • 30,156
  • 15
  • 86
  • 87
  • I had to do this for iOS 11. Setting contentOffset alone worked for iOS 9/10 for 0 or more cells, and it also worked on iOS 11 for 8+ cells. Anything less required this solution. Is there a reason why? – Tony Nov 09 '17 at 06:22
  • I think the first paragraph of this post explains why. – Brian Nov 11 '17 at 10:13
  • I understood the first paragraph, but it was still strange that it only was "broken" on iOS 11 for me. I would have expected similar behavior for iOS 9, 10, and 11 when the number of cells are the same (less than enough). Thanks again and sorry for bringing up an old post! – Tony Nov 16 '17 at 01:02
4

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

grep
  • 566
  • 5
  • 20
  • You should really not hard code your points, why don't you use `CGRectGetHeight(searchBar.bounds)`? – Zorayr Mar 09 '15 at 05:23
  • I guess you must be doing it in viewDidLoad() / init() where no frame has been set for you to get the height of the search bar. May be you should try with viewDidLayoutSubviews() – Kaushil Ruparelia Mar 22 '16 at 16:15
  • If the tableView has not finished loading, none of the `ContentOffset` solutions will work – Maor Sep 02 '17 at 11:45
4

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.

RyanJM
  • 7,028
  • 8
  • 60
  • 90
3

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)];
}
dibi
  • 3,257
  • 4
  • 24
  • 31
user3246173
  • 488
  • 6
  • 18
2

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};
}
p0lAris
  • 4,750
  • 8
  • 45
  • 80
  • Good solution for the offset problem. Works for me when using a UINavigationController. – Andreas Kraft Jan 09 '15 at 16:12
  • 1
    The solution doesn't work when the number of rows does not cover the entire screen - would yo know a workaround for this? – Zorayr Mar 09 '15 at 18:07
  • Not sure I understand this completely. This works for me when I have just a few rows in the table. However I do use the following: `self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];`. – p0lAris Apr 30 '15 at 21:30
2

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)
    }
Confused Vorlon
  • 9,659
  • 3
  • 46
  • 49
1

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 :-)

Chilly Zhong
  • 16,763
  • 23
  • 77
  • 103
1

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

LightMan
  • 3,517
  • 31
  • 31
1

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.

kbpontius
  • 3,867
  • 1
  • 30
  • 34
1

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
        }
drv
  • 788
  • 9
  • 16
1

Below code is working for me

self.tableView.contentOffset = CGPointMake(0.0, 44.0);
Ashu
  • 3,373
  • 38
  • 34
1

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

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.

fujianjin6471
  • 5,168
  • 1
  • 36
  • 32
0

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)
stakahop
  • 921
  • 9
  • 18
0

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)
}
Johannes Knust
  • 891
  • 1
  • 11
  • 18
0

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"];
}
slobodans
  • 859
  • 1
  • 17
  • 23