Locking a UISearchBar to the top of a UITableView like Game Center
Asked Answered
T

7

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.

Tibia answered 2/2, 2011 at 2:0 Comment(0)
C
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;
}
Curable answered 12/3, 2011 at 21:0 Comment(11)
Oh haha yeah, I noticed that. Thanks very much for that! That's the code I ended up going with. ^_^Tibia
I guess it should be MAX instead of MIN.Pleione
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.Curable
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.Lint
On my phone (ios6), there is no visible difference between doing MIN and 0.Tisbee
@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.Khachaturian
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?Photojournalism
@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 youAcceptation
It seems that doing this in iOS 9 makes touch go through the header view to the table view cells.Patrol
@SteveMoser I noticed that too. Do you found a solution for that?Angus
@KevinLieser Nope :/Patrol
S
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.

Scrub answered 3/2, 2011 at 16:8 Comment(4)
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. :)Tibia
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.Tibia
+1 This should be accepted answer as other doesn't work. Thax It helped.Offprint
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?Angus
F
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
       }
Faria answered 22/1, 2016 at 14:49 Comment(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.Labe
C
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.

Converse answered 28/9, 2016 at 3:10 Comment(0)
M
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.

Monotheism answered 29/6, 2013 at 12:51 Comment(8)
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?Corrosive
Hi @Rushi- I haven't used this code yet in iOS 7. When I do, I'll post an update with any changes.Monotheism
@Rushi: I am having same kind of issue, have you got solution for iOS 7?Gott
@Corrosive 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)Axenic
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?Moria
@M.Porooshani are you able to find a solution yet? Having same problem here. Thanks.Farmyard
@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.Moria
@Farmyard any solution for that problem?Angus
K
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.

Khachaturian answered 10/6, 2014 at 23:5 Comment(2)
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?Graecize
@SAHM, are you able to find a solution yet? Having same problem here. Thanks.Farmyard
I
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.

Imperception answered 17/2, 2017 at 10:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.