swiftuikitviper-architecture

Custom Table View outlets throwing nil


I am trying to implement a simple iOS app using the VIPER pattern following this tutorial. The app is a simple table view which displays data from a json req

I have created the table view and can successfully show data from my object using the default cell.textLabel. I am however trying to create a custom table view cell and so created the nib and class. I have connected all the outlets up correctly to the class from the nib and the code for this class is as follows:

class CustomTableViewCell: UITableViewCell {
    
    static let identifier = "CustomTableView"
    
    static func nib() -> UINib {
        return UINib(nibName: "CustomTableViewCell", bundle: nil)
    }

    @IBOutlet var level: UILabel!
    @IBOutlet var levelNumber: UILabel!
    @IBOutlet var progress: UIProgressView!
    @IBOutlet var leftProgress: UILabel!
    @IBOutlet var rightProgress: UILabel!
    
    public func configure(levelString: String, levelNum: String, prog: Float, left: String, right: String) {
        level.text = levelString
        levelNumber.text = levelNum
        progress.setProgress(prog, animated: true)
        leftProgress.text = left
        rightProgress.text = right
    }

    override func awakeFromNib() {
        super.awakeFromNib()
    }
}

The problem is, when I run the app, these outlets are erroring: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value on level.text inside the configure function. I can suppress this error by doing this in my view: table.register(CustomTableViewCell.nib(), forCellReuseIdentifier: CustomTableViewCell.identifier) however this just shows an empty tableView without the custom rows or any data. If I do table.register(CustomTableViewCell.self, forCellReuseIdentifier: CustomTableViewCell.identifier) then this crashes. What way should I be doing it and how can I get the data to display in the tableView? relevant code added below:

let tableView: UITableView = {
    let table = UITableView()
    table.register(CustomTableViewCell.nib(), forCellReuseIdentifier: CustomTableViewCell.identifier)
    table.isHidden = true
    return table
}()

override func viewDidLoad() {
    super.viewDidLoad()
    view.addSubview(tableView)
    tableView.delegate = self
    tableView.dataSource = self
}

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    tableView.frame = view.bounds
    label.frame = CGRect(x: 0, y: 0, width: 200, height: 50)
    label.center = view.center
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return achievements.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    
    let cell = tableView.dequeueReusableCell(withIdentifier: CustomTableViewCell.identifier, for: indexPath) as! CustomTableViewCell
    cell.configure(levelString: "Level",
                     levelNum: achievements[indexPath.row].level,
                     prog: Float(achievements[indexPath.row].progress/achievements[indexPath.row].total),
                     left: String(achievements[indexPath.row].progress),
                     right: String(achievements[indexPath.row].total))
    return cell
}

Outlet Setup:

Outlet setup

Files Owner:

Files owner


Solution

  • I appreciate this might not be the correct answer for everyone, but this is what worked for me. I was struggling with trying to debug this and for the sake of time, I concluded that just creating everything programmatically would be easier. There are numerous tutorials for this, but I took the following structure: Stevia library used to set constraints for components and add subview

    import Stevia
    import UIKit
    
    class CustomTableViewCell: UITableViewCell {
        
        static let identifier = "CustomTableViewCell"
        
        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            setupFieldsAndConstraints()
        }
        
        func setupFieldsAndConstraints() {
            subviews {
                customLabel
            }
            setupCustomLabel()
        }
        
        var customLabel: UILabel = {
            let level = UILabel()
            return level
        }()
        
        func setupCustomLabel() {
            customLabel.style { l in
                l.textAlignment = .left
                l.textColor = .white
                l.Top == 50
                l.Left == 20
            }
        }
        
        func setValues(object: Object) {
            label.text = object.name
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    }
    

    And this is referenced in your view as it normally would be. I chose to add mine to a function that encapsulates any table configuration and then called in viewDidLoad:

    tableView.register(CustomTableViewCell.self, forCellReuseIdentifier: CustomTableViewCell.identifier)
    

    And then for the cellForRowAt:

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: CustomTableViewCell.identifier) as! CustomTableViewCell
        cell.setValues(object: Object[indexPath.row])
        return cell
    }