Jeffrey
Jeffrey

Reputation: 1271

Using a programmatically created UITableViewCell subclass in Swift

I'm working on a table view with a custom cell that has been created programmatically, with no usage of IB. I've looked around on Google and asked around in NSChat, but I still can't get it to work. It just displays the default table view. Thanks in advance!

EventCell.swift

import UIKit

class EventCell: UITableViewCell {

    var eventName: UILabel = UILabel()
    var eventCity: UILabel = UILabel()
    var eventTime: UILabel = UILabel()

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        self.contentView.addSubview(eventName)
        self.contentView.addSubview(eventCity)
        self.contentView.addSubview(eventTime)
    }

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        eventName = UILabel(frame: CGRectMake(20, 10, self.bounds.size.width - 40, 25))
        eventCity = UILabel(frame: CGRectMake(0, 0, 0, 0))
        eventTime = UILabel(frame: CGRectMake(0, 0, 0, 0))

    }

}

ViewController.swift

class ViewController: UITableViewController, UITableViewDelegate {

    var events: Dictionary<String, [String]> = ["0": ["Monroe Family", "La Cañada", "8:30"]]

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self;
        tableView.delegate = self;

        tableView.registerClass(EventCell.self, forCellReuseIdentifier: "EventCell")
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()

    }

    override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        return UITableViewAutomaticDimension;
    }

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

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

        var cellIdendifier: String = "EventCell"

        var cell: EventCell = tableView.dequeueReusableCellWithIdentifier(cellIdendifier, forIndexPath: indexPath) as EventCell
        cell = EventCell(style: .Default, reuseIdentifier: cellIdendifier)

        if let i = events[String(indexPath.row)] {
            cell.eventName.text = i[0]
            cell.eventCity.text = i[1]
            cell.eventTime.text = i[2]
        }

        cell.sizeToFit()

        return cell


    }
}

Upvotes: 32

Views: 51495

Answers (4)

Suragch
Suragch

Reputation: 511626

I had the same question. I applied the advice in @drewag's answer, but there were some additional issues. Below is a bare-bones, proof-of-concept example that shows how to use a programmatically created UITableViewCell subclass.

The image below is what it should look like. The yellow rectangles are UILabel subviews in the custom cells.

enter image description here

Code

Here is the UITableViewCell subclass. It's job is to initialize the cell and its content, add the subviews, and layout the subviews.

import UIKit
class MyCustomCell: UITableViewCell {

    var myLabel = UILabel()

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        myLabel.backgroundColor = UIColor.yellowColor()
        self.contentView.addSubview(myLabel)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        myLabel.frame = CGRect(x: 20, y: 0, width: 70, height: 30)
    }
}

Here is the view controller code.

import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    @IBOutlet weak var tableView: UITableView!

    let myArray = ["one", "two", "three", "four"]
    let cellReuseIdendifier = "cell"


    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.registerClass(MyCustomCell.self, forCellReuseIdentifier: cellReuseIdendifier)

        tableView.dataSource = self
        tableView.delegate = self
    }

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

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

        let cell = tableView.dequeueReusableCellWithIdentifier(cellReuseIdendifier, forIndexPath: indexPath) as! MyCustomCell
        cell.myLabel.text = myArray[indexPath.row]

        return cell
    }
}

Notes:

  • I used a regular UIViewController rather than a UITableViewController. If you use a UITableViewController then remove the UITableViewDelegate and UITableViewDataSource protocol references since these are redundant for a UITableViewController. You also won't need the @IBOutlet tableView line.
  • The main problem line I found in the original question was eventName = UILabel(frame: CGRectMake(...)). Instead it should have been eventName.frame = CGRect(...)

Upvotes: 29

johndpope
johndpope

Reputation: 5249

You can use lazy instantiation in swift to avoid having to drop into layoutSubviews / view life cycle dance.

class EventCell: UITableViewCell {
  lazy public  var lblName = {
    return UILabel (frame: CGRectMake(10, 0, self.bounds.size.width , 40))
    }()

 lazy public  var lblCity = {
    return UILabel (frame: CGRectMake(10, 40, self.bounds.size.width , 20))
    }()
 lazy public  var lblTime = {
    return UILabel (frame: CGRectMake(self.bounds.size.width - 80, 10, , 25))
    }()


  override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)

    self.contentView.addSubview(lblName)
    self.contentView.addSubview(lblCity)
    self.contentView.addSubview(lblTime)
  }

} 

Upvotes: 3

Cheyne Mathey-Owens
Cheyne Mathey-Owens

Reputation: 41

Your particular code does not work because it is creating new UILabels in EventCell.layoutSubviews. Only the initial UILabels are getting added to the view tree and their sizes are probably 0'd.

I'm guessing you meant to update their frames, e.g. update the EventCell class method:

override func layoutSubviews() {
    super.layoutSubviews()

    eventName.frame = CGRectMake(20, 10, self.bounds.size.width - 40, 25)
    eventCity.frame = CGRectMake(<actual size>)
    eventTime.frame = CGRectMake(<actual size>)
}

Upvotes: 3

drewag
drewag

Reputation: 94703

Ok first a few important comments.

First, you are needlessly creating your own new cell every time the table view requests a cell instead of reusing old cells. You should remove this line:

cell = EventCell(style: .Default, reuseIdentifier: cellIdendifier)

That is unnecessary because dequeue will automatically create new cells as needed based on the class you have registered for the given identifier.

Second, you should not be using the main screen bounds when laying out your code. This will break down if your table view is not the full width. Instead, you can use self.bounds so it is always relative to the cell itself.

Third, you should not be calling setNeedsLayout or layoutIfNeeded because if that method is called, it is already laying out everything again.

Fourth, you should register your table view cell class before setting the table view data source just in case UITableView every starts requesting things from data source when the data source is set.

Fifth, two of your subviews have a size of 0,0 so they are not going to show up anyway.

If this code is indeed running without crashing then you are creating EventCells because you are doing a forced casting from the result of the dequeueReusableCellWithIdentifier:forIndexPath. That means you simply have a layout / display / data issue.

Upvotes: 31

Related Questions