swiftuitableviewxibnib

UITableViewHeaderFooterView with xib(nib)


I wrote the source code at the bottom.

I gave File's Owner the IconHeader class and after connecting the imageView as an IBOutlet, when I run the application, my application crashes and I get the following error.

enter image description here

Thread 1: "[<NSObject 0x6000030c8860> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key iconImageView."

Then I removed ImageView from inpector and deleted IconHeader in File's Owner from class and added IconHeader to View's class. I chose Icon Header from Object tab while connecting ImageView.

enter image description here

This time, when I run the application, the following text appears on the console. "Then I removed ImageView from inpector and deleted IconHeader in File's Owner from class and added IconHeader to View's class. I chose Icon Header from Object tab while connecting ImageView."

enter image description here

MainViewController:

final class MainViewController: UIViewController {

    @IBOutlet private weak var tableView: UITableView!
    
    var viewModel: MainViewModelProtocol? {
        didSet {
            viewModel?.delegate = self
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        preapreTableView()
        registerCellsForTableView()
        registerHeaderForTableView()
        viewModel?.fetchAgents()
    }
    
    private func preapreTableView() {
        tableView.delegate = self
        tableView.dataSource = self
    }
    
    private func registerCellsForTableView() {
        let descriptionCellName = String(describing: DescriptionCell.self)
        let descriptionCellNib = UINib(nibName: descriptionCellName, bundle: .main)
        tableView.register(descriptionCellNib, forCellReuseIdentifier: descriptionCellName)
    }
    
    private func registerHeaderForTableView() {
        let iconHeaderName = String(describing: IconHeader.self)
        let iconHeaderNib = UINib(nibName: iconHeaderName, bundle: nil)
        tableView.register(iconHeaderNib, forHeaderFooterViewReuseIdentifier: iconHeaderName)
    }
}

MainViewController Extension:

extension MainViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        guard let section = viewModel?.cellType[section]
        else { return nil }
        if case .description = section {
            guard let iconHeader = tableView.dequeueReusableHeaderFooterView(withIdentifier: String(describing: IconHeader.self)) as? IconHeader
            else { return nil }
            return iconHeader
        }
        return nil
    }
}

IconHeader:

final class IconHeader: UITableViewHeaderFooterView {
    
    @IBOutlet weak var iconImageView: UIImageView!
    
    override init(reuseIdentifier: String?) {
        super.init(reuseIdentifier: reuseIdentifier)
        commonInit()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
        fatalError("init(coder:) has not been implemented")
    }
    
    private func commonInit() {
        if let view = loadViewFromNib() {
            view.frame = bounds
            view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
            contentView.backgroundColor = .red
            contentView.addSubview(view)
        }
    }
    
    private func loadViewFromNib() -> UIView? {
        let nibName = String(describing: IconHeader.self)
        let nib = UINib(nibName: nibName, bundle: nil)
        return nib.instantiate(withOwner: self, options: nil).first as? UIView
    }
}

Solution

  • First, you're doing more work than needed...

    Let's start with changing the IconHeader to this:

    final class IconHeader: UITableViewHeaderFooterView {
        
        @IBOutlet var iconImageView: UIImageView!
        
    }
    

    Next we create a new, Empty User Interface File:

    enter image description here

    and save it as IconHeader (with default .xib extension).

    Add a plain UIView:

    enter image description here

    and set Simulated Metrics -> Size to Freeform:

    enter image description here

    Resize it to make it easy to work with (the size for the moment doesn't matter), and assign its class to IconHeader:

    enter image description here

    Drag in a UIImageView ... constrain it centered, at 30x30 ... give it a systemYellow background so we can see its frame, and any image (I used the Swift SF Symbol with Red Tint color) ... and connect the @IBOutlet:

    enter image description here

    Now let's use a simplified view controller so we can run it without your viewModel. We'll use a default UITableViewCell class, 10 sections, 2 rows in each section:

    final class MainViewController: UIViewController {
        
        let tableView = UITableView()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            tableView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(tableView)
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                tableView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
                tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
                tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
                tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
            ])
            
            preapreTableView()
            registerCellsForTableView()
            registerHeaderForTableView()
        }
        
        private func preapreTableView() {
            tableView.delegate = self
            tableView.dataSource = self
        }
        
        private func registerCellsForTableView() {
            // register a default cell
            tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        }
        
        private func registerHeaderForTableView() {
            let iconHeaderName = String(describing: IconHeader.self)
            let iconHeaderNib = UINib(nibName: iconHeaderName, bundle: nil)
            tableView.register(iconHeaderNib, forHeaderFooterViewReuseIdentifier: iconHeaderName)
        }
        
    }
    
    extension MainViewController: UITableViewDataSource {
        
        func numberOfSections(in tableView: UITableView) -> Int {
            return 10
        }
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return 2
        }
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let c = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
            c.textLabel?.text = "\(indexPath)"
            return c
        }
        
    }
    
    extension MainViewController: UITableViewDelegate {
        
        func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
            
            guard let iconHeader = tableView.dequeueReusableHeaderFooterView(withIdentifier: String(describing: IconHeader.self)) as? IconHeader
            else { return nil }
            return iconHeader
            
        }
        
    }
    

    Running that, we get:

    enter image description here

    Unfortunately, even though we are not setting any background colors, you will see this error / warning message for each section header in the debug console:

    [TableView] Changing the background color of UITableViewHeaderFooterView 
      is not supported. Use the background view configuration instead.
    

    To get rid of that, in your xib, change the view's background color from System Background to Default:

    enter image description here

    No more error messages.

    To actually set the background color, we can do that either in viewForHeaderInSection:

    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        
        guard let iconHeader = tableView.dequeueReusableHeaderFooterView(withIdentifier: String(describing: IconHeader.self)) as? IconHeader
        else { return nil }
    
        iconHeader.contentView.backgroundColor = .cyan
        
        return iconHeader
        
    }
    

    Output:

    enter image description here

    or, we can set a default color in the IconHeader cell class init:

    final class IconHeader: UITableViewHeaderFooterView {
        
        @IBOutlet var iconImageView: UIImageView!
        
        override init(reuseIdentifier: String?) {
            super.init(reuseIdentifier: reuseIdentifier)
            commonInit()
        }
        
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        
        private func commonInit() {
            contentView.backgroundColor = .green
        }
    
    }
    

    enter image description here