TIMEX
TIMEX

Reputation: 271774

How do I make an expandable UITableView header?

I want to display half of the header at first. When the user taps on one of the buttons inside the header, the rest of the header slides down and reveals itself. When the user taps on the button again, the header slides back up to its original size (1/2 size).

However, when I tried to expand my header's height, it covers the tableView cells instead of pushing them down.

func prepareHeader(){
    let headerHeight = CGFloat(51.0)
    headerView = UIView(frame: CGRect(x: 0, y: 0, width: screenWidth, height: headerHeight))
    headerView.backgroundColor = UIColor.whiteColor()
    headerView.layer.addSublayer(bottomBorder)
    headerView.layer.masksToBounds = true


    let toggler = UIButton(type: .Custom)
    toggler.frame = CGRectMake(0, 0, 40, 40)
    toggler.backgroundColor = FlatGreen()
    toggler.addTarget(self, action: "toggleHeader:", forControlEvents: UIControlEvents.TouchUpInside)
    headerView.addSubview(toggler)

    self.tableView.tableHeaderView = headerView

}

func toggleHeader(sender: UIButton){
    print("Pushed")
    var newFrame = self.headerView.frame
    newFrame.size.height = CGFloat(100)
    self.headerView.frame = newFrame
}

Upvotes: 2

Views: 2152

Answers (2)

paulvs
paulvs

Reputation: 12053

WHAT WE WANT

A table view header that can be dynamically opened and closed.

enter image description here

SOLUTION 1: WITHOUT INTERFACE BUILDER


class ViewController: UIViewController {

    var headerView = UIView()
    var tableView  = UITableView()
    var isHeaderOpen = false
    let HEADER_CLOSED_HEIGHT: CGFloat = 100
    let HEADER_OPEN_HEIGHT: CGFloat = 200

    var headerClosedHeightConstraint: NSLayoutConstraint!
    var headerOpenHeightConstraint: NSLayoutConstraint!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Add the table view to the screen.
        view.addSubview(tableView)

        // Make the table view fill the screen.
        tableView.translatesAutoresizingMaskIntoConstraints = false
        view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[tableView]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["tableView": tableView]))
        view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[tableView]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["tableView": tableView]))

        // Add the header view to the table view.
        tableView.tableHeaderView = headerView
        headerView.backgroundColor = UIColor.redColor()
        headerView.translatesAutoresizingMaskIntoConstraints = false

        // Add a button to the header.
        let button = UIButton(type: .Custom)
        button.setTitle("Toggle", forState: .Normal)
        headerView.addSubview(button)
        button.translatesAutoresizingMaskIntoConstraints = false
        headerView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[button]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["button": button]))
        headerView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-[button]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["button": button]))
        button.addTarget(self, action: "toggleHeaderAction:", forControlEvents: .TouchUpInside)

        // Make the header full width.
        view.addConstraint(NSLayoutConstraint(item: headerView, attribute: .Width, relatedBy: .Equal, toItem: view, attribute: .Width, multiplier: 1, constant: 0))

        // Create the constraints for the two different header heights.
        headerClosedHeightConstraint = NSLayoutConstraint(item: headerView, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 0, constant: HEADER_CLOSED_HEIGHT)

        // Create the height constraint for the header's other height for later use.
        headerOpenHeightConstraint = NSLayoutConstraint(item: headerView, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: HEADER_OPEN_HEIGHT)

        closeHeader()   // Close header by default.
    }

    func openHeader() {
        headerView.removeConstraint(headerClosedHeightConstraint)
        headerView.addConstraint(headerOpenHeightConstraint)
        updateHeaderSize()
        isHeaderOpen = true
    }

    func closeHeader() {
        headerView.removeConstraint(headerOpenHeightConstraint)
        headerView.addConstraint(headerClosedHeightConstraint)
        updateHeaderSize()
        isHeaderOpen = false
    }

    func updateHeaderSize() {
        // Calculate the header's new size based on its new constraints and set its frame accordingly.
        let size = headerView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
        var frame = headerView.frame
        frame.size.height = size.height
        headerView.frame = frame
        tableView.tableHeaderView = headerView
        self.headerView.layoutIfNeeded()
    }

    func toggleHeader() {
        if isHeaderOpen {
            closeHeader()
        } else {
            openHeader()
        }
    }

    func toggleHeaderAction(sender: AnyObject) {
        toggleHeader()
    }
}

SOLUTION 2: USING INTERFACE BUILDER


To make a UITableView header change height dynamically, try the following.

In your storyboard, first add a UITableView to your view controller. Then add a UIView as the first child of the UITableView (Interface Builder automatically interprets this as the UITableView's header view).

enter image description here

Wire up an outlet to the table and header so we can access them later in code.

@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var headerView: UIView!

Next, in code we create two constraints (opened and closed) which we'll later add/remove to toggle the header's height.

var headerOpenHeightConstraint: NSLayoutConstraint!
var headerClosedHeightConstraint: NSLayoutConstraint!

Let's also create a flag to keep track of the open/closed state of the header. Also, we specify the open header height.

var isHeaderOpen = false
let HEADER_OPEN_HEIGHT: CGFloat = 200

Interface Builder automatically converts the dimensions of the header we created into Auto Layout constraints. Since we're going to change the header's height ourselves, we need to add replacement constraints of our own and remove these automatic constraints (NSAutoresizingMaskLayoutConstraints). In viewDidLoad(), add the following (takes the default header height as the closed height and HEADER_OPEN_HEIGHT as the open header height).

override func viewDidLoad() {
    super.viewDidLoad()

    // The header needs a new width constraint since it will no longer have the automatically generated width.
    view.addConstraint(NSLayoutConstraint(item: headerView, attribute: .Width, relatedBy: .Equal, toItem: view, attribute: .Width, multiplier: 1, constant: 0))

    // Add the height constraint for the default state (closed header).
    headerClosedHeightConstraint = NSLayoutConstraint(item: headerView, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: headerView.bounds.height)
    headerView.addConstraint(headerClosedHeightConstraint)

    // Make the constraint we'll use later to open the header.
    headerOpenHeightConstraint = NSLayoutConstraint(item: headerView, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: HEADER_OPEN_HEIGHT)

    // Finally we disable this so that we don't get Interface Builder's automatically generated constraints
    // messing with our constraints.
    headerView.translatesAutoresizingMaskIntoConstraints = false
}

Finally, in Interface Builder, add a button and wire it up to an action in your view controller.

@IBAction func toggleHeaderAction(sender: AnyObject) {

    // Add/remove the appropriate constraints to toggle the header.
    if isHeaderOpen {
        headerView.removeConstraint(headerOpenHeightConstraint)
        headerView.addConstraint(headerClosedHeightConstraint)
    } else {
        headerView.removeConstraint(headerClosedHeightConstraint)
        headerView.addConstraint(headerOpenHeightConstraint)
    }

    // Calculate the header's new size based on its new constraints.
    let size = headerView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)

    // Give the header its new height.
    var frame = headerView.frame
    frame.size.height = size.height
    headerView.frame = frame
    headerView.setNeedsLayout()
    headerView.layoutIfNeeded()
    tableView.tableHeaderView = headerView

    isHeaderOpen = !isHeaderOpen
}

Upvotes: 5

sschale
sschale

Reputation: 5188

A couple thoughts to try

  • Right after you set it, call self.tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition .Top:, animated: true) where indexPath is the cell that was previously at the top? You can experiment with it being animated or not
  • From your description I can't tell if you're having issue with the content size offsets from the new frame (aka cells are actually under and you can't scroll up to them). If that's the case, would setting self.tableView.headerView again improve that?

Upvotes: 0

Related Questions