This Old App: Custom Table View Cells

A few months ago, Apple removed my app Interval RunCalc from the App Store for being too old. In this series of articles I’ll document what I did on rebuilding and improving the app. In the last installment, I connected the model to the controller and was shocked to find they actually worked on the first attempt, leaving me in awe of the effects of planning out my code carefully. I’m ready for the next part for the application – the intervals. To start that I’ll need a custom table view cell.

The Storyboard

If you’ve never used custom table view cells before, Let me quickly get you up to speed. They are subviews used as a table view cell. You put whatever views you want in them, and set up a view controller to handle them. You start the whole process by changing from a basic cell to a custom cell.

Then you design your cell. I added three buttons and two labels. The interval will work similar to time, distance and pace I’ve already defined.

I realize I’ve forgotten something: locks. I’ll ask myself a question first: What kind of data do people put in intervals?

Time & Pace = Distance Distance & pace = Time

I don’t compute pace. Pace is always an input in an interval. This leads me to a different locking mechanism than the root view controller: when I input a Time, I lock distance, when I input a distance I lock time.

How do I tell the user which one is locked? There’s two possibilities. I could do the same as the root controller, with some sort of indicator. That requires real estate on an already small space. Another option is changing the font or color of the locked value. I change the alpha of the locked value. For an interval it is the least useful data. Bringing attention to the other two by diminishing the locked value makes sense. For example a locked time would look like this:

I can code the dimming later, but this will work, and leave a very visible pattern in the table for time and distance intervals. I could do more with the background colors here too, but I’ll wait on that. I want to see with a lot of data what it looks like before I pick how to work with color.

Making and Connecting IntervalTableViewCell

With a cell designed, I make a new class IntervalTableViewCell, subclassing UITableViewCell. I associate the cell with the table view in the identity inspector like any other controller.

I’ll use the assistant editor to hook up the outlets. Here’s one of those wrinkles I mentioned earlier: Table view cells are not automatic in the assistant editor.

You will manually navigate to the table view cell

I’ve found it sometimes after this first association shows up in Automatic, but not always. I always use Manual to get to the file.

Once the assistant editor has the right file, I make the connections.

@IBOutlet weak var timeEntry: UIButton!
  @IBOutlet weak var distanceEntry: UIButton!
  @IBOutlet weak var distanceUnits: UILabel!
  @IBOutlet weak var paceEntry: UIButton!
  @IBOutlet weak var paceUnits: UILabel!
//MARK: - Actions

  @IBAction func paceEntry(_ sender: UIButton) {
  }

  @IBAction func distanceEntry_ sender: UIButton) {
  }

  @IBAction func timeEntry(_ sender: UIButton) {
  }

While very similar to the root controller, there’s a few differences. Lock is missing since I’m not using those indicators. The units are labels not buttons. The units will inherit from the root. There is just not enough space for changing units here.

Testing the cell

There’s two ways to handle updating my custom cell. ONe is I can set the outlets and actions in the tableview:cellForRowWithIndexpath:, the second is within the cell. I’ll try the first of those to test the cell. In IntervalTableViewController, I change tableview:cellForRowWithIndexpath: to this:

 override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCell(
      withIdentifier: "cell", for: indexPath)  
      as! IntervalTableViewCell
    //let row = indexPath.row
    //cell.textLabel?.text = "Row \(row)"
    return cell
  }

While I also commented out the two demo lines from earlier, the key change is down casting the cell to class IntervalTableViewCell from its generic UITableViewCell. Running this with one row, I get:

 

which is a good sign. Now I’ll check the outlets, adding this code:

let row = indexPath.row
    cell.distanceEntry.setTitle("\(row)", for: .normal)
    cell.paceEntry.setTitle("10:00", for: .normal)
    cell.timeEntry.setTitle("05:45:23", for: .normal)
    cell.distanceUnits.text = "mile"
    cell.paceUnits.text = "min/mile"

I’ll set the number of rows to 5 in the data source, then run

Passing a Model

The outlets are working. I could set up my computations in the IntervalTableViewController, but that gets messy. That’s what caused version 1 to be such a mess. I’m going to pass a RunStats object to the table view cell, letting the cell do all the work.

In the cell, I initialize a property runStats.

var runStats = RunStats()

In the cell, I’ll write a method updateCellDisplay to display the values in runStats. Most of this looks familiar to code I’ve already written. The first part of the method is the time display.

//Time

    let timeTitle = runStats.time.hoursMinutesSeconds()

    timeEntry.setTitle(timeTitle, for: .normal)

The second the Distance

//Distance

    let distanceTitle = runStats.distance.displayString()
    distanceEntry.setTitle(distanceTitle, for: .normal)
    switch runStats.distance.displayUnits{
    case .kilometers:
      distanceUnits.text = "kilometers"
    case .miles:
      distanceUnits.text = "miles"
    }

Finally, there’s the pace

//Pace

    let paceTitle = runStats.pace.displayString()
    paceEntry.setTitle(paceTitle, for: .normal)


    switch runStats.pace.displayUnits{
    case .kilometersPerHour:
      paceUnits.text = "Km/hr"
    case .minutesSecondsPerMile:
      paceUnits.text = "min/mi"
    case .milesPerHour:
      paceUnits.text = "mi/hr"
    }


 

I’ll add this method to the cell’s awakeFromNib: 

// update the cell
    updateCellDisplay()

I’ll test with some set data which I can easily cut and paste, into awakeFromNib:

runStats.pace = PaceStat(minutes: 10, seconds: 00)

runStats.pace.displayUnits = .minutesSecondsPerMile

runStats.distance = DistanceStat(miles: 26.2)

runStats.distance.displayUnits = .miles

runStats.locked = .time

runStats.time = runStats.time(pace: runStats.pace, distance: runStats.distance)

I’ll build and run from that

This works inside the cell. I try cutting and pasting this data to the IntervalTableViewController’s tableView:CellForRowAtIndexpath:….And that doesn’t work.

The cell is already initialized so the cell didn’t update. Adding

cell.updateCellDisplay()

does the trick.

I’ll want that to be more automatic. So I’ll move updateCellDisplay call here in the cell’s controller as a property observer:

var runStats = RunStats(){

    didSet{

      updateCellDisplay()

    }

  }

Which also works. Any time runStats changes, I’ll update the display. That will be every time the table assigns a cell.

I’ve got a working table view cell. What I haven’t done yet is connect it to the Root controller, since the table controller is a subview of a container view. In the next installment, I’ll get the communications between root and table set up.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s