将UISearchBar固定在UITableView顶部像Game Center一样

28
在Game Center中的UITableView和它们顶部的搜索栏有一个很酷的功能。与将搜索栏放置在表头视图中(因此计为标准表格单元格)的应用程序不同,它似乎被螺栓固定到其上面的父导航栏。因此,在滚动表格时,搜索栏确实会移动,但如果您向上滚动超出表格范围,搜索栏将永远不会停止接触导航栏。
有人知道这可能是如何完成的吗?我想知道苹果是否将搜索栏和表格都放在一个父滚动视图中,但我想知道是否可能比那更简单。
7个回答

28

Bob 的答案颠倒了:正确的应该是 MIN(0, scrollView.contentOffset.y)。

此外,为了正确支持调整大小(当旋转时会发生),其他框架值应该被重用。

-(void)scrollViewDidScroll:(UIScrollView *)scrollView 
{
    UISearchBar *searchBar = searchDisplayController.searchBar;
    CGRect rect = searchBar.frame;
    rect.origin.y = MIN(0, scrollView.contentOffset.y);
    searchBar.frame = rect;
}

1
MIN 应该是正确的,因为我们想要的行为是防止搜索栏滚动超过视图顶部。当用户向上滚动(视图向下移动)并且 contentOffset.y 变为负数时,我们希望通过将搜索栏框架的 y 坐标设置为相同的(负数)值来抵消这种情况,从而导致搜索栏在负滚动期间保持固定。这仅适用于 contentOffset.y 为负数时,因此使用 MIN。 - LinenIsGreat
14
UISearchBar 作为 tableHeaderView 时,在 -scrollViewDidScroll: 中直接操纵它的框架(Frame) 在 iOS 5.x 上可以工作,但在 iOS 6 上不行。为了使其正常工作,我不得不将搜索栏包装在一个普通的 UIView 中,并将其添加为表头。 - followben
在我的手机(ios6)上,使用MIN和0没有明显的区别。 - Chase Roberts
@sms .... 如果你正在使用故事板来处理UITableViewController,那么它可能无法正常工作....甚至tableview的headerview也无法固定在表格上...如果你解决了这个问题,请告诉我..谢谢。 - Bandish Dave
在iOS 9中这样做似乎会使触摸事件穿过标题视图传递到表格视图单元格。 - Steve Moser
显示剩余7条评论

7

您可以将搜索栏放在表头,并为表格视图实现 - (void)scrollViewDidScroll:(UIScrollView *)scrollView 委托方法。像这样做应该可以:

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

如果您使用了searchDisplayController,那么您可以通过self.searchDisplayController.searchbar访问搜索栏。

谢谢你,鲍勃! 是的,我在发布问题后昨天和一些朋友交流过,我们基本上想到了同样的解决方案。虽然我正在考虑子类化UITableView并在其中完成操作,而不必触及委托。如果我完成了代码,我会在这里发布它。 :) - TiM
嘿,伙计。上面被接受的答案中的代码是我最终采用的。这只是拦截UITableView委托的滚动事件的问题。 - TiM
+1 这应该被接受为正确答案,因为其他的不起作用。谢谢,它帮了我很大的忙。 - Janak Nirmal
1
当我滚动表格视图并点击搜索栏时,他会触发“didSelectAtTableRow”事件并打开详细视图,而不是打开键盘开始搜索。他会触发搜索栏后面的单元格点击事件。有人有想法吗? - Kevin Lieser

5
在Swift 2.1和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
       }

1
你确定当你在表视图上滚动时,搜索栏可以使用吗?因为当我尝试搜索时,只有当表视图在顶部时才能进行搜索...如果我向下滚动一点,就无法搜索了。 - Konstantinos Natsios

4
虽然其他答案似乎有所帮助,部分完成了工作,但它未解决搜索栏不接收用户触摸的问题,因为随着您更改其框架,它会移动到其父视图的边界之外。
更糟糕的是,当你点击搜索栏使其成为第一响应者时,很可能tableView代理将在搜索栏下方布置的单元格上调用tableView:didSelectRowAtIndexPath:。
为了解决上述问题,您需要将搜索栏包装在一个简单的UIView中,该视图能够处理发生在其边界之外的触摸。通过这种方式,您可以将这些触摸传递给搜索栏。
因此,让我们首先这样做:
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)
  }
}

现在,我们有一个名为SearchBarView的UIView子类,它能够接收发生在其边界之外的触摸事件。

其次,在视图控制器加载其视图时,我们应该将搜索栏放入这个新视图中:

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
  }
}

最后,我们应该更新搜索栏的框架,使其随着用户滚动表视图而保持固定在顶部:
override func scrollViewDidScroll(scrollView: UIScrollView) {
  searchBar.frame.origin.y = max(0, scrollView.contentOffset.y)
}

以下是结果:

输入图像描述

--

重要提示:如果你的表视图有分组,它们可能会遮挡搜索栏,因此每当表视图的bounds更新时,你需要将搜索栏置于它们之上。

输入图像描述

viewDidLayoutSubviews是一个不错的位置来完成此操作:

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

--

希望这能帮到你。你可以从这里下载示例项目。

3

如果您想完全模拟Game Center中的搜索栏,则需要进行一步操作。

如果您使用friedenberg的优秀答案以及评论中提到的iOS 6+的修正版本followben,您仍然需要调整搜索栏本身处于活动状态时的功能。

在Game Center中,当您向下滚动表格时,搜索栏将随之滚动,但是当您尝试向上滚动超出表格边界时,搜索栏将保持固定在导航栏下方。但是,当搜索栏处于活动状态且正在显示搜索结果时,搜索栏不再随表格滚动; 它保持在导航栏下方固定不动

这是实现此功能的完整代码(适用于iOS 6+)。 (如果您的目标是iOS 5或更低版本,则不需要在UIView中包装UISearchBar)

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

现在,当搜索栏处于非活动状态时,它将随着表格一起移动,并在尝试超出表格边界时保持固定。当激活时,它将像游戏中心一样保持固定位置。


1
嗨@Nick,这段代码在iOS 7上无法工作。当我拖动表格时,我看不到UISearchBar。有什么建议吗?您能告诉我应该为UISearchbar设置什么框架吗? - Rushi
嗨@Rushi-我还没有在iOS 7中使用过这段代码。当我使用后,我会发布更新并说明任何更改。 - Nick Schneble
1
@Rushi 你是否在UINavigationController中使用它?在这种情况下,新的iOS7“导航栏下内容”就会发挥作用。scrollView.contentOffset.y也包含此偏移量,因此当在tableview顶部时,实际上会返回-64而不是0。因此,MIN行应该写成MIN(0, scrollView.contentOffset.y + scrollView.contentInset.top) - Tyson
1
我可以按照您在iOS 8.3下描述的方式使其正常工作,但是在滚动后,触摸搜索栏不会使其成为第一响应者,但是,如果我将其滚动回到原始的搜索栏偏移量位置,它又开始正常工作了!您是如何解决这个问题的? - M. Porooshani
@M.Porooshani,你找到解决方案了吗?我这里也有同样的问题。谢谢。 - Anthony
显示剩余3条评论

0

这里的所有其他答案都为我提供了有用的信息,但是在使用iOS 7.1时,它们都不起作用。以下是一个对我有效的简化版本:

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

注意:如果您在列表上使用“下拉刷新”,则需要在scrollViewDidScroll:中将scrollView.contentInset.top替换为常量,以允许搜索栏在刷新动画上滚动。

4
在iOS 8中似乎无法正常工作。搜索栏确实会显示出来,并且它的框架也按照预期移动,但当搜索栏“粘着”在屏幕上并且uitableview单元格滑动到其下面时,接收用于搜索栏的触摸事件的是那些位于搜索栏下方的单元格,这样会导致问题。我对此有误解吗? - SAHM
@SAHM,你找到解决方案了吗?我这里也有同样的问题。谢谢。 - Anthony

0
如果您的部署目标是 iOS 9 及更高版本,则可以使用锚点并以编程方式设置 UISearchBar 和 UITableView:
    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)
            ])
    }

我假设您是通过编写代码而非在故事板中创建UISearchBar和UITableView。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接