28

There's this cool feature in the UITableViews in Game Center and the search bars they have at their tops. Unlike apps where the search bar is placed in the table header view (so it counts as a standard table cell), instead, it seems to be bolted to the parent navigation bar above it. So when scrolling the table, the search bar does indeed move, but if you scroll above the boundaries of the table, the search bar never stops touching the navigation bar.

Does anyone know how this might have been done? I was wondering if Apple maybe placed both the search bar and the table in a parent scroll view, but I'm wondering if it may be simpler than that.

nevan king
  • 112,709
  • 45
  • 203
  • 241
TiM
  • 15,812
  • 4
  • 51
  • 79

7 Answers7

28

Bob's answer is reversed: it ought to be MIN(0, scrollView.contentOffset.y).

Also, in order to properly support resizing (which would occur when rotated), the other frame values should be reused.

-(void)scrollViewDidScroll:(UIScrollView *)scrollView 
{
    UISearchBar *searchBar = searchDisplayController.searchBar;
    CGRect rect = searchBar.frame;
    rect.origin.y = MIN(0, scrollView.contentOffset.y);
    searchBar.frame = rect;
}
LinenIsGreat
  • 594
  • 4
  • 13
  • Oh haha yeah, I noticed that. Thanks very much for that! That's the code I ended up going with. ^_^ – TiM Mar 15 '11 at 15:29
  • I guess it should be MAX instead of MIN. – msbrogli Aug 11 '11 at 04:06
  • 1
    MIN should be correct, as the behavior we're seeking is to prevent the search bar from scrolling past the top of the view. When the user scrolls up (the view moves down) and contentOffset.y becomes negative. We want to counteract this by setting the y origin of the search bar frame to be the same (negative) value, which causes the search bar to remain fixed during negative scrolling. This only applies when contentOffset.y is negative, hence MIN. – LinenIsGreat Jul 24 '12 at 15:43
  • 14
    When the `UISearchBar` is the `tableHeaderView`, direct manipulation of it's frame in `-scrollViewDidScroll:` works on iOS 5.x but not on iOS 6. To get it working, I had to wrap the search bar in a plain `UIView` and add that as the table header. – followben Oct 18 '12 at 06:06
  • followben 's answer helped me! :) Thanks! – scurioni Jan 13 '13 at 01:09
  • On my phone (ios6), there is no visible difference between doing MIN and 0. – Chase Roberts Jan 29 '14 at 04:05
  • @friedenberg At least in iOS 7.1, pushing the UITableView up causes contentOffset.y to become positive, so the correct behavior is achieved by using MAX rather than MIN. Also, in my case, the offset is being calculated from the screen origin rather than the view origin, so I had to use `MAX(0, scrollView.contentOffset.y + 64)` to account for the 20 point status bar and 44 point navigation bar. – Brian Jun 10 '14 at 21:56
  • but in iOS 8 enabling auto layout makes the search bar disappear on clicking on it.I wrapped a search bar in uiview and added as tableview header from storyboard.But it does not works.any solution? – SMS Dec 09 '14 at 10:52
  • @sms .... if you are working with UITableViewController through story board then its not working....even headerview of tableview is not stick on table...if you solve this problem then plz let me know.. thank you – Bandish Dave Apr 23 '15 at 10:11
  • It seems that doing this in iOS 9 makes touch go through the header view to the table view cells. – Steve Moser Sep 17 '15 at 19:44
  • @SteveMoser I noticed that too. Do you found a solution for that? – Kevin Lieser Mar 06 '16 at 06:24
  • @KevinLieser Nope :/ – Steve Moser Mar 07 '16 at 04:17
7

You could put the searchBar in the table header and implement the - (void)scrollViewDidScroll:(UIScrollView *)scrollView delegate method for the tableView. Doing something like this should work:

-(void) scrollViewDidScroll:(UIScrollView *)scrollView {
    searchBar.frame = CGRectMake(0,MAX(0,scrollView.contentOffset.y),320,44);
} 

If you used the searchDisplayController, you would access the searchbar using self.searchDisplayController.searchbar.

Bob Vork
  • 2,927
  • 28
  • 32
  • Thanks for that Bob! Yeah I was talking to some friends yesterday after posting the question and we came up with basically the same thing. Although, I was looking at subclassing UITableView and doing it in there, without having to touch the delegate. If I complete the code, I'll post it up here. :) – TiM Feb 04 '11 at 06:32
  • Hey man. The code in the accepted answer above is what I ultimately rolled with. It's just a matter of intercepting the scroll events with the UITableView's delegate. – TiM Feb 04 '12 at 06:22
  • +1 This should be accepted answer as other doesn't work. Thax It helped. – Janak Nirmal Jul 24 '12 at 04:58
  • 1
    When I scroll my table view and then tap the search bar he triggers the "didSelectAtTableRow" event and opens detail view instead of opening keyboard to start searching. He triggers the cell tap behind the search bar. Anyone has an idea? – Kevin Lieser Mar 06 '16 at 06:22
5

In Swift 2.1 and iOS 9.2.1

    let searchController = UISearchController(searchResultsController: nil)

        override func viewDidLoad() {
        /* Search controller parameters */
            searchController.searchResultsUpdater = self  // This protocol allows your class to be informed as text changes within the UISearchBar.
            searchController.dimsBackgroundDuringPresentation = false  // In this instance,using current view to show the results, so do not want to dim current view.
            definesPresentationContext = true   // ensure that the search bar does not remain on the screen if the user navigates to another view controller while the UISearchController is active.

            let tableHeaderView: UIView = UIView.init(frame: searchController.searchBar.frame)
            tableHeaderView.addSubview(searchController.searchBar)
            self.tableView.tableHeaderView = tableHeaderView

        }
        override func scrollViewDidScroll(scrollView: UIScrollView) {
           let searchBar:UISearchBar = searchController.searchBar
           var searchBarFrame:CGRect = searchBar.frame
           if searchController.active {
              searchBarFrame.origin.y = 10
           }
           else {
             searchBarFrame.origin.y = max(0, scrollView.contentOffset.y + scrollView.contentInset.top)

           }
           searchController.searchBar.frame = searchBarFrame
       }
Muhammad Qasim
  • 159
  • 2
  • 7
  • 1
    Are you sure that when you scroll at the table view, the search bar can be used? Because when i try to search, i can do it only when the tableview is at the top... if i scroll a bit down i cant search. – Konstantinos Natsios Dec 24 '16 at 12:53
4

While other answers seem helpful and partially do the job, it doesn't solve the issue of search bar not receiving the user's touches because it moves outside the bounds of its parent view as you change its frame.

What's worse is that, when you click on the search bar to make it the first responder, it is very likely that the tableView delegate will call tableView:didSelectRowAtIndexPath: on cell that is laid out under the search bar.

In order to address those issues described above, you need to wrap the search bar in a plain UIView, a view which is capable of processing touches occurred outside of its boundaries. By this way, you can relay those touches to the search bar.

So let's do that first:

class SearchBarView: UIView {
  override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
    for subview in subviews {
      if !subview.userInteractionEnabled { continue }

      let newPoint = subview.convertPoint(point, fromView: self)
      if CGRectContainsPoint(subview.bounds, newPoint) {
        return subview.hitTest(newPoint, withEvent: event)
      }
    }
    return super.hitTest(point, withEvent: event)
  }
}

Right now, we have a UIView subclass named SearchBarView which is capable of receiving touches occurred outside of its boundaries.

Secondly, we should put the search bar into that new view while the view controller is loading its view:

class TableViewController: UITableViewController {
  private let searchBar = UISearchBar(frame: CGRectZero)
  ...

  override func viewDidLoad() {
    super.viewDidLoad()
    ...
    searchBar.sizeToFit()
    let searchBarView = SearchBarView(frame: searchBar.bounds)
    searchBarView.addSubview(searchBar)

    tableView.tableHeaderView = searchBarView
  }
}

At last, we should update the frame of the search bar as user scrolls down the table view so that it will stay fixed at the top:

override func scrollViewDidScroll(scrollView: UIScrollView) {
  searchBar.frame.origin.y = max(0, scrollView.contentOffset.y)
}

Here is the result:

enter image description here

--

Important note: If your table view has sections, they will probably shadow your search bar so you need to bring the search bar on top of them every time the table view's bounds gets updated.

enter image description here

viewDidLayoutSubviews is a good place to do that:

override func viewDidLayoutSubviews() {
  super.viewDidLayoutSubviews()
  ...
  if let tableHeaderView = tableView.tableHeaderView {
    tableView.bringSubviewToFront(tableHeaderView)
  }
}

--

Hope this helps. You can download the example project from here.

Ozgur Vatansever
  • 49,246
  • 17
  • 84
  • 119
3

There's one more step if you want to fully emulate the search bars in Game Center.

If you start with friedenberg's excellent answer, as well as followben's modification for iOS 6+ mentioned in the comments, you still need to adjust the functionality when the search bar itself is active.

In Game Center, the search bars will scroll with the table as you scroll down, but will remain fixed below the navigation bar when you attempt to scroll up past the boundaries of the table. However, when the search bar is active and search results are being displayed, the search bar no longer scrolls with the table; it remains fixed in place below the navigation bar.

Here's the complete code (for iOS 6+) for implementing this. (If you're targeting iOS 5 or below, you don't need to wrap the UISearchBar in a UIView)

CustomTableViewController.m

- (void)viewDidLoad
{
    UISearchBar *searchBar = [[UISearchBar alloc] init];
    ...
    UIView *tableHeaderView = [[UIView alloc] initWithFrame:searchBar.frame];
    [tableHeaderView addSubview:searchBar];

    [self.tableView setTableHeaderView:tableHeaderView];
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    UISearchBar *searchBar = self.tableView.tableHeaderView.subviews.lastObject;
    CGRect searchBarFrame = searchBar.frame;

    /*
     * In your UISearchBarDelegate implementation, set a boolean flag when
     * searchBarTextDidBeginEditing (true) and searchBarTextDidEndEditing (false)
     * are called.
     */

    if (self.inSearchMode)
    {
        searchBarFrame.origin.y = scrollView.contentOffset.y;
    }
    else
    {
        searchBarFrame.origin.y = MIN(0, scrollView.contentOffset.y);
    }

    searchBar.frame = searchBarFrame;
}

- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar
{
    self.inSearchMode = YES;
}

- (void)searchBarTextDidEndEditing:(UISearchBar *)searchBar
{
    self.inSearchMode = NO;
}

Voilà! Now, when the search bar is inactive it will move with the table, and remain fixed when attempting to move beyond the table boundaries. When active, it will remain fixed in place, just like in Game Center.

  • 1
    Hi @Nick This code is not working on iOS 7. I can't see UISearchBar when I drag the table. Any suggestion? Can you tell me what frame i should set to UISearchbar? – Rushi Oct 04 '13 at 09:34
  • Hi @Rushi- I haven't used this code yet in iOS 7. When I do, I'll post an update with any changes. – Nick Schneble Oct 08 '13 at 09:22
  • @Rushi: I am having same kind of issue, have you got solution for iOS 7? – Mrunal Oct 09 '13 at 06:37
  • 1
    @Rushi Are you using it within a `UINavigationController`? In this case the new iOS7 'content under the nav bar' comes into play. The scrollView.contentOffset.y also contains this offset, so will actually return -64 rather than 0 when at the top of the tableview. Therefor the MIN line should read `MIN(0, scrollView.contentOffset.y + scrollView.contentInset.top)` – Tyson Nov 07 '13 at 04:09
  • 1
    I could make it work the way you described under iOS 8.3 but after scrolling, touching the search bar does not make it first responder, but, if I scroll all the way back up, (to the original, intended offset of search bar) it starts working again! How are you tackling this? – M. Porooshani May 03 '15 at 10:04
  • @M.Porooshani are you able to find a solution yet? Having same problem here. Thanks. – Anthony Oct 07 '15 at 16:00
  • @Anthony, I'm afraid, no. I've been away from that project ever since. But from what I have investigated, this must be a notorious bug in iOS 8 (I'm not sure about iOS 9). If I ever get to solve it, I'll let you know if it's not too late. – M. Porooshani Oct 07 '15 at 17:45
  • @Anthony any solution for that problem? – Kevin Lieser Mar 06 '16 at 06:27
0

All of the other answers here provided me with helpful information, but none of them worked using iOS 7.1. Here's a simplified version of what worked for me:

MyViewController.h:

@interface MyViewController : UIViewController <UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate, UISearchDisplayDelegate> {
}
@end

MyViewController.m:

@implementation MyViewController {

    UITableView *tableView;
    UISearchDisplayController *searchDisplayController;
    BOOL isSearching;
}

-(void)viewDidLoad {
    [super viewDidLoad];

    UISearchBar *searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, 320, 44)];
    searchBar.delegate = self;

    searchDisplayController = [[UISearchDisplayController alloc] initWithSearchBar:searchBar contentsController:self];
    searchDisplayController.delegate = self;
    searchDisplayController.searchResultsDataSource = self;
    searchDisplayController.searchResultsDelegate = self;

    UIView *tableHeaderView = [[UIView alloc] initWithFrame:searchDisplayController.searchBar.frame];
    [tableHeaderView addSubview:searchDisplayController.searchBar];
    [tableView setTableHeaderView:tableHeaderView];

    isSearching = NO;
}

-(void)scrollViewDidScroll:(UIScrollView *)scrollView {

    UISearchBar *searchBar = searchDisplayController.searchBar;
    CGRect searchBarFrame = searchBar.frame;

    if (isSearching) {
        searchBarFrame.origin.y = 0;
    } else {
        searchBarFrame.origin.y = MAX(0, scrollView.contentOffset.y + scrollView.contentInset.top);
    }

    searchDisplayController.searchBar.frame = searchBarFrame;
}

- (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller {
    isSearching = YES;
}

-(void)searchDisplayControllerWillEndSearch:(UISearchDisplayController *)controller {
    isSearching = NO;
}

@end

Note: If you're using "pull down to refresh" on your list, you'll need to replace scrollView.contentInset.top in scrollViewDidScroll: with a constant to allow the search bar to scroll over the refresh animation.

Brian
  • 14,610
  • 7
  • 35
  • 43
  • 4
    This does not seem to work in iOS 8. The search bar is indeed displayed, and its frames move as expected, but when the search bar 'sticks' and the uitableview cells slide under it, it is whichever cell is under the search bar at the time that receives the touches meant for the search bar. Am I wrong about this? – SAHM Dec 07 '14 at 02:44
  • @SAHM, are you able to find a solution yet? Having same problem here. Thanks. – Anthony Oct 07 '15 at 15:53
0

If your deployment target is iOS 9 and higher then you can use anchors and set UISearchBar and UITableView programmatically:

    private let tableView = UITableView(frame: .zero, style: .plain)
    private let searchBar = UISearchBar(frame: CGRect .zero)

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.delegate = self
        tableView.dataSource = self
        tableView.contentInset = UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0)

        searchBar.delegate = self
        view.addSubview(tableView)
        view.addSubview(searchBar)
        NSLayoutConstraint.activate([
            searchBar.heightAnchor.constraint(equalToConstant: 44.0),
            searchBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            searchBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            searchBar.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor)
            ])
    }

I assume that you create UISearchBar and UITableView from code, not in storyboard.

Filipp Ignatov
  • 141
  • 1
  • 8