iosswiftuitableviewuitextviewuitableviewsectionheader

Dynamic section header height on runtime


I have UITableView in view controller with a section header and in the header, I have one UITextView with scroll disabled and pinned all UITextView edges to its super view.enter image description here

Here is the code for automatic height change

func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
    guard let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: "CreatePostHeaderView") as? CreatePostHeaderView else {
        return nil
    }
    
    return view
}

func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
    return UITableView.automaticDimension
}

And also set the estimated Height with this code

 tableView.estimatedSectionHeaderHeight = 75

But on runtime when the text of UITextView exceeds height of 75, it doesn't change after that regardless of UITextView content. So, Do I need to add anything to make sure the table section header height changed according to UITextView content? Am I missing anything here?


Solution

  • When performing some action that changes the height of a cell (including header / footer cells), you have to inform the table view that the height has changed.

    This is commonly done with either:

    tableView.beginUpdates()
    tableView.endUpdates()
    

    or:

    tableView.performBatchUpdates(_:completion:)
    

    In this case, you want to call this when the text in your text view changes - easily done with a "callback" closure.

    Here is an example of using a UITextView in a reusable UITableViewHeaderFooterView.

    This will apply to loading a complex view from a XIB, but since this view is simple (only contains a UITextView), we'll do it all from code. This example uses 3 sections, each with 12 rows (default table view cells).

    First, the table view controller class - no @IBOutlet or @IBAction connections, so just create a new UITableViewController and set its custom class to MyTestSectionHeaderTableViewController:

    class MyTestSectionHeaderTableViewController: UITableViewController {
        
        var myHeaderData: [String] = [
            "Section 0",
            "Section 1",
            "Section 2",
        ]
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            tableView.rowHeight = 50
            
            tableView.keyboardDismissMode = .onDrag
            
            tableView.sectionHeaderHeight = UITableView.automaticDimension
            tableView.estimatedSectionHeaderHeight = 75
            
            tableView.register(UITableViewCell.self, forCellReuseIdentifier: "defCell")
            tableView.register(MySectionHeaderView.self, forHeaderFooterViewReuseIdentifier: MySectionHeaderView.reuseIdentifier)
        }
        
        override func numberOfSections(in tableView: UITableView) -> Int {
            return myHeaderData.count
        }
        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return 12
        }
        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let c = tableView.dequeueReusableCell(withIdentifier: "defCell", for: indexPath)
            c.textLabel?.text = "\(indexPath)"
            return c
        }
        override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
            
            let v = tableView.dequeueReusableHeaderFooterView(withIdentifier: MySectionHeaderView.reuseIdentifier) as! MySectionHeaderView
            
            v.myTextView.text = myHeaderData[section]
            
            v.textChangedCallback = { txt in
                self.myHeaderData[section] = txt
                tableView.performBatchUpdates(nil, completion: nil)
            }
            
            return v
    
        }
        
    }
    

    and this is the UITableViewHeaderFooterView class. Note that it needs to conform to UITextViewDelegate so we can tell the controller the text has changed (so it can update the height when needed), and we pass back the newly edited text to update our data source:

    class MySectionHeaderView: UITableViewHeaderFooterView, UITextViewDelegate {
        static let reuseIdentifier: String = String(describing: self)
        
        var myTextView: UITextView = {
            let v = UITextView()
            v.isScrollEnabled = false
            return v
        }()
        
        var textChangedCallback: ((String) -> ())?
        
        override init(reuseIdentifier: String?) {
            super.init(reuseIdentifier: reuseIdentifier)
            commonInit()
        }
        
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            commonInit()
        }
        
        func commonInit() -> Void {
            
            contentView.addSubview(myTextView)
            
            myTextView.translatesAutoresizingMaskIntoConstraints = false
            
            let g = contentView.layoutMarginsGuide
            
            NSLayoutConstraint.activate([
                myTextView.topAnchor.constraint(equalTo: g.topAnchor),
                myTextView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
                myTextView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
                myTextView.bottomAnchor.constraint(equalTo: g.bottomAnchor)
            ])
    
            myTextView.delegate = self
        }
        
        func textViewDidChange(_ textView: UITextView) {
            guard let str = textView.text else {
                return
            }
            textChangedCallback?(str)
        }
        
    }
    

    The result:

    enter image description here