Auto-layout does not automatically update the size of a table header view, so we need to do it "manually."
We can use this extension to help:
extension UITableView {
    func sizeHeaderToFit() {
        guard let headerView = tableHeaderView else { return }
        let height = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
        var frame = headerView.frame
        
        // avoids infinite loop!
        if height != frame.height {
            frame.size.height = height
            headerView.frame = frame
            tableHeaderView = headerView
        }
    }
}
Now, when we update the content of the table header view - which would cause its height to change - we can call .sizeHeadrToFit()
Here's a complete example:
Simple multiline cell - cyan label
class MultilineCell: UITableViewCell {
    
    let label: UILabel = {
        let v = UILabel()
        v.numberOfLines = 0
        return v
    }()
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        label.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(label)
        let g = contentView.layoutMarginsGuide
        NSLayoutConstraint.activate([
            // constrain label to the cell's margins guide
            label.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
            label.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
            label.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
            label.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
        ])
        label.backgroundColor = .cyan
    }
    
}
Multiline view for table header - yellow label in a red view
class MyTableHeaderView: UIView {
    
    let label: UILabel = {
        let v = UILabel()
        v.numberOfLines = 0
        return v
    }()
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        label.translatesAutoresizingMaskIntoConstraints = false
        addSubview(label)
        let g = self.layoutMarginsGuide
        NSLayoutConstraint.activate([
            // constrain label to the self's margins guide
            label.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
            label.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
            label.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
        ])
        // this avoids auto-layout complaints
        let c = label.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0)
        c.priority = UILayoutPriority(rawValue: 999)
        c.isActive = true
        
        backgroundColor = .red
        label.backgroundColor = .yellow
    }
    
}
Example table view controller
class DynamicHeaderTableViewController: UITableViewController {
    
    var theData: [String] = []
    
    let myHeaderView = MyTableHeaderView()
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 10 rows with 2-to-5 lines per row
        for i in 1...10 {
            let s = "This is row \(i)"
            let n = Int.random(in: 1...4)
            let a = (1...n).map { "Line \($0)" }
            theData.append(s + "\n" + a.joined(separator: "\n"))
        }
        
        tableView.register(MultilineCell.self, forCellReuseIdentifier: "cell")
        
        myHeaderView.label.text = "Select a row..."
        tableView.tableHeaderView = myHeaderView
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        tableView.sizeHeaderToFit()
    }
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return theData.count
    }
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let c = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MultilineCell
        c.label.text = theData[indexPath.row]
        return c
    }
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        myHeaderView.label.text = theData[indexPath.row]
        tableView.sizeHeaderToFit()
    }
}
The above code will generate 10 rows with 2 to 5 lines per row. On didSelectRowAt we'll update the table view header with the text from the row.
Result when first launched:

After selecting "Row 2":

After selecting "Row 3":
