Is it possible to disable floating headers in UITableView with UITableViewStylePlain?

Viewed 139385

I'm using a UITableView to layout content 'pages'. I'm using the headers of the table view to layout certain images etc. and I'd prefer it if they didn't float but stayed static as they do when the style is set to UITableViewStyleGrouped.

Other then using UITableViewStyleGrouped, is there a way to do this? I'd like to avoid using grouped as it adds a margin down all my cells and requires disabling of the background view for each of the cells. I'd like full control of my layout. Ideally they'd be a "UITableViewStyleBareBones", but I didn't see that option in the docs...

Many thanks,

31 Answers

You should be able to fake this by using a custom cell to do your header rows. These will then scroll like any other cell in the table view.

You just need to add some logic in your cellForRowAtIndexPath to return the right cell type when it is a header row.

You'll probably have to manage your sections yourself though, i.e. have everything in one section and fake the headers. (You could also try returning a hidden view for the header view, but I don't know if that will work)

There is another tricky way. The main idea is to double the section number, and first one only shows the headerView while the second one shows the real cells.

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return sectionCount * 2;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    if (section%2 == 0) {
        return 0;
    }
    return _rowCount;
}

What need to do then is to implement the headerInSection delegates:

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
    if (section%2 == 0) {
        //return headerview;
    }
    return nil;
}

- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
    if (section%2 == 0) {
        //return headerheight;
    }
    return 0;
}

This approach also has little impact on your datasources:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    int real_section = (int)indexPath.section / 2;
    //your code
}

Comparing with other approaches, this way is safe while not changing the frame or contentInsets of the tableview. Hope this may help.

A variation on @samvermette's solution:

/// Allows for disabling scrolling headers in plain-styled tableviews
extension UITableView {

    static let shouldScrollSectionHeadersDummyViewHeight = CGFloat(40)

    var shouldScrollSectionHeaders: Bool {
        set {
            if newValue {
                tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: bounds.size.width, height: UITableView.shouldScrollSectionHeadersDummyViewHeight))
                contentInset = UIEdgeInsets(top: -UITableView.shouldScrollSectionHeadersDummyViewHeight, left: 0, bottom: 0, right: 0)
            } else {
                tableHeaderView = nil
                contentInset = .zero
            }
        }

        get {
            return tableHeaderView != nil && contentInset.top == UITableView.shouldScrollSectionHeadersDummyViewHeight
        }
    }

}

The snippet display a sticky header only for the first section. Others section headers will float with a cells.

func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {

    if section == 1 {
        tableView.contentInset = .zero
    }

}

func tableView(_ tableView: UITableView, didEndDisplayingHeaderView view: UIView, forSection section: Int) {
    if section == 0 {
        tableView.contentInset = .init(top: -tableView.sectionHeaderHeight, left: 0, bottom: 0, right: 0)
    }
}

A tricky way is add an empty section for header. Because section has no cell, it will not floating at all.

**Swift 5.3 | Programmatically **

private func buildTableView() -> UITableView {
    let tableView = UITableView()
    tableView.translatesAutoresizingMaskIntoConstraints = false
    tableView.rowHeight = UITableView.automaticDimension
    tableView.showsVerticalScrollIndicator = false
    tableView.separatorStyle = .none
    let dummyViewHeight: CGFloat = 80
    tableView.tableFooterView = UIView(
        frame: CGRect(x: .zero,
                      y: .zero,
                      width: tableView.bounds.size.width,
                      height: dummyViewHeight))

    tableView.contentInset = UIEdgeInsets(top: .zero, left: .zero, bottom: -dummyViewHeight, right: .zero)
    return tableView
}

Best solution for below 2 scenarios:

Scenario 1: If the tableView style is not .grouped. Have no problem with changing it to grouped. footer wont float.

Scenario 2: If you want only one footer at the end of a tableView. and It's style is .plain

Steps:

  1. add one more section at the end.
  2. make sure in added section have (nuberOfRowsInSection method) zero number of rows.
  3. add custom footer to that section.
  4. set heightForFooterInSection to your custom footer and set .zero to other sections' footers.

example :

func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {

//note: Filter the section where you intended to add custom section

    let footerView = UIView()
    submitOrganiser?.showFooterAtTheEnd(view: footerView) //my method to customise the footer, use your implementation
    return footerView
}

If you don't mind using a UICollectionView, this can also be achieved using UICollectionView list configurations in iOS 13.0+ and using headerMode set to .firstItemInSection.

override func viewDidLoad() {
    super.viewDidLoad()
    var config = UICollectionLayoutListConfiguration.init(appearance: .plain)
    config.headerMode = .firstItemInSection
    let listLayout = UICollectionViewCompositionalLayout.list(using: config)
    self.collectionView.collectionViewLayout = listLayout
}

you can easily achieve this by implementing the viewForHeaderInSection method in the tableview delegate class. this method expects a UIView as return object (which is your header view). i have done this same thing in my code

Related