14

I'd like to be able to fix the position of certain rows in a UITableView as the user scrolls.

Specifically, I have a table whereby certain rows are "headers" for the rows that follow, and I'd like the header to stay at the top of the screen as the user scrolls up. It would then move out of the way when the user scrolls far enough that the next header row would take its place.

A similar example would be the Any.DO app. The "Today", "Tommorrow" and "Later" table rows are always visible on the screen.

Does anyone have any suggestions about how this could be implemented?

I'm currently thinking of follow the TableDidScroll delegate and positioning my own cell in the appropriate place in front of the table view. The problem is that at other times I'd really like these cells to be real table cells so that they can be, for example, reordered by the user.

Thanks,

Tim

tarmes
  • 15,366
  • 10
  • 53
  • 87
  • 1
    why don't you use UITableView headers? You can put text there or make a custom view. There are methodes of UITableViewDelegate for that – Novarg Jun 05 '12 at 09:24

3 Answers3

13

I've been playing about with this and I've come up with a simple solution.

First, we add a single UITableViewCell property to the controller. This should be initialize such that looks exactly like the row cells that we'll use to create the false section headers.

Next, we intercept scrolling of the table view

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    // Add some logic here to determine the section header. For example, use 
    // indexPathsForVisibleRows to get the visible index paths, from which you 
    // should be able to get the table view row that corresponds to the current 
    // section header. How this works will be implementation dependent.
    //
    // If the current section header has changed since the pervious scroll request 
    // (because a new one should now be at the top of the screen) then you should
    // update the contents.

    IndexPath *indexPathOfCurrentHeaderCell = ... // Depends on implementation
    UITableViewCell *headerCell = [self.tableView cellForRowAtIndexPath:indexPathOfCurrentHeaderCell];

    // If it exists then it's on screen. Hide our false header

    if (headerCell)
        self.cellHeader.hidden = true;

    // If it doesn't exist (not on screen) or if it's partially scrolled off the top,
    // position our false header at the top of the screen

    if (!headerCell || headerCell.frame.origin.y < self.tableView.contentOffset.y )
    {
        self.cellHeader.hidden = NO;
        self.cellHeader.frame = CGRectMake(0, self.tableView.contentOffset.y, self.cellHeader.frame.size.width, self.cellHeader.frame.size.height);
    }

    // Make sure it's on top of all other cells

    [self.tableView bringSubviewToFront:self.cellHeader];
}

Finally, we need to intercept actions on that cell and do the right thing...

Raptor
  • 53,206
  • 45
  • 230
  • 366
tarmes
  • 15,366
  • 10
  • 53
  • 87
  • Thanks a lot, tarmes. I am looking for the similar thing and need also implement your logic in my application. However, i could not get what is the headerCell you mean? And when you mean the headerCell is on the screen? – Hai Jun 30 '14 at 12:06
  • The headerCell is the cell in the table view that you wish to stay at the top of the table as it's scrolled. – tarmes Jun 30 '14 at 13:41
  • Question: You say you use this to be able to reorder the headers. Do you collapse the content cells to make the header reordering easier? I was thinking about it but not sure if it's possible, as `reloadData` or other type of table view update would probably interrupt /revert the floating header cell. – User Oct 18 '15 at 18:05
  • To be clear - just in case - I mean when user taps on the handler to start dragging the header, in this moment I would like to "close" the sections, so only headers are visible, and it's easier to place the header in its new position. I would need to trigger `reloadData` or updates in order to collapse the sections when user starts dragging, which I think would cancel the dragging. – User Oct 18 '15 at 18:08
  • If not possible alternatively could have a separate button where the user can manually collapse all the headers and then reorder... but it would be neat if this can be done automatically, when the user starts dragging. – User Oct 18 '15 at 18:10
3

That's the default behavior for section headers in plain UITableView instances. If you want to create a custom header, implement the tableView:viewForHeaderInSection: method in your table view delegate and return the view for your header.

Although you will have to manage sections and rows instead of just rows.

Fran Sevillano
  • 8,103
  • 4
  • 31
  • 45
  • The problem is that I'd like to be able to recorder the section headers. As far as I can tell there's no UI that'll allow the user to do that in the same way as they can currently drag section rows about... – tarmes Jun 05 '12 at 09:26
  • I see, you will have to handle that yourself. Seems a little complicated but I am sure it can be done. It seems like there is nothing implemented out there. Maybe I can help you with it and we can put it on Github or something. – Fran Sevillano Jun 05 '12 at 09:31
  • Interestingly, there is now a moveSection:toSection: call in iOS5, but I don't think there's a way to initiate this move from the UI... – tarmes Jun 05 '12 at 09:45
  • You can add a button or a gesture recognizer to the header view. That way you can get when the section is selected. – Fran Sevillano Jun 05 '12 at 10:02
0

Swift 5 solution

var header: UIView?

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(indexPath: indexPath) as UITableViewCell
    header = cell.contentView
    return cell
}

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let headerCell = tableView.cellForRow(at: IndexPath(row: 0, section: 0))
    guard headerCell == nil || (headerCell!.frame.origin.y < self.tableView.contentOffset.y + headerCell!.frame.height/2) else {
        header?.isHidden = true
        return
    }
    guard let hdr = header else { return }
    hdr.isHidden = false
    hdr.frame = CGRect(x: 0, y: tableView.contentOffset.y, width: hdr.frame.size.width, height: hdr.frame.size.height)
    if !tableView.subviews.contains(hdr) {
        tableView.addSubview(hdr)
    }
    tableView.bringSubviewToFront(hdr)
}
Mike Karpenko
  • 51
  • 1
  • 2