This Old App:Use Multiple Custom Cells

A few months ago, Apple removed my app Interval RunCalc from the App Store for being too old and not updated. In this series of articles I’ll document what I did on rebuilding and improving the app. In the last installment, I got the custom table view talking with the root view. This time I’m adding multiple cells to the table. I’ll address a really important question along the way: how to know what cell to use.

Working with a New Model: RunStatsIntervals

Back when we built models for the app, I made more than just the RunStats model. I made a second model class which contained a array of RunStats called RunStatsIntervals.

var intervals = [RunStats]()

It contained methods for totaling the stat. What it didn’t contain is a method for returning a RunStat for those totals. That will come in handy for passing to view controller’s runStat PROPERTY, so I’ll add that to the methods in the model:

func totalStats()->RunStats{

    let runstat = RunStats()

    runstat.pace = avgPace()

    runstat.time = totalTime()

    runstat.distance = totalDistance()

    return runstat

  }


While doing that I see one more problem with a division by zero error in averagePace, so I change

let averagePace = totalDistance / totalTime

to

var averagePace = 0.0
    if totalTime != 0{
        averagePace = totalDistance / totalTime
}

So the function returns zero instead of a division by zero error.

I’ll now head over the IntervalTableViewController and add the model there:

  var statsIntervals = RunStatsIntervals()

I’ll change the data sources to read from the intervals list my number of rows is now

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

    return statsIntervals.intervals.count

  }

My number of sections remains 1, and my cells will now be from the intervals array of statsIntervals. Cleaning up my code of all the test code I used in the last installment, I get 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.runStats = statsIntervals.intervals[row]

    return cell

  }


Running that code, I get this:

Which is what I expect. For that first interval, I’ll Go back to RootViewController and add another instance of the model

var runStatsIntervals = RunStatsIntervals()

In ViewDidLoad, I’ll make that first cell which is all zeroes equal the root’s display:

runStatsIntervals.intervals[0] = runStats

In prepare:forSegue: I send the model to the table’s viewController:

 tableViewController.statsIntervals = runStatsIntervals

Run again and I get a single cell with the root’s values.

Adding More Cells

To add another cell, I have a button on the bottom toolbar. That will activate in RootController. The root controller then has to tell the table view to add a new cell by adding an element to the statsIntervals.intervals array, then updating the display.

First I’ll make an action for the button. I left myself some comments here which I realize are completely obsolete. This is how I dealt with it in version 1:

 @IBAction func addInterval(_ sender: UIButton) {

 //step 1 instantiate an instance of runstats

// step 2 show an action sheet to pick between time/pace,time/distance or distance/speed. use the selection to lock the runstats.

//step 3 set units to reflect runstats

//step 4 add to runstats

//step 5 update table and totals     

}

Version 2 does a lot of this in the table view controller or a delegate method. I got rid of step 2 by the buttons on the cells to change time, pace and distance. I set up the segue to have a pointer to the table view controller, so what looks like a lot of work is really only this:

@IBAction func addInterval(_ sender: UIButton) {

    tableViewController.addCell()

  }

Then the IntervalTableView controller has a method

  func addCell(){

    statsIntervals.add(stat: RunStats())

    updateDisplay()

  }

This is ONE of those times I get a smile on my face for good planning. I run, press the plus and I get this:

Finding the Cell indexPath

Still feeling the nice warm glow of getting a lots of work done in three lines of code, I realize the next big hurdle. The second cell does nothing. I can try to change the numbers there, but nothing changes. The reason why is simple. Take a look at the updateDisplay method:

  func updateDisplay(){

    runStats.recalculate()

    tableView.reloadData()

    delegate.didChangeStats(stat: runStats, controller: self)

  }

I’m still updating the display for the one cell, runStats, I used as a test. I want the array, not the one cell. That’s true in the Delegate pickerDidFinish too. I used runStats, not the interval.

Here comes the big question of the day: That interval needs an index. How do I get the index? Since I’m hitting the button and not the cell underneath it, I can’t get an index from didSelectRowAtIndexpath. I start thinking this out and come up with some crazy solutions to use the delegate to get my index.

Then I get sane. There has to be a way to do this since every social media app uses something like it to work properly. I browse the documentation. In the UITableViewdocumentation I find this gem:

func indexPath(for cell: UITableViewCell) -> IndexPath?

This takes the cell and returns the index path. I just need the cell itself. I’ll be needing this in only one situation: when I press a button in a table view cell. Going to the view hierarchy I see this:

The superview of the button is the content view. the superview of the content view is the cell. So I can get the superview of the super view, cast it as a UITableViewCell and use the indexPathForCell: method to get an index. I’ll do this for all three buttons, so I’ll write a function

 func currentRow(button:UIButton){

    let cell = button.superview?.superview as! IntervalTableViewCell

    guard let indexPath = tableView.indexPath(for: cell) else{

        print("not a cell")

        return

      }

    print("Selected Row \(indexPath.row)")

    currentRow = indexPath.row

     

  }

I added a few print statements for testing. That function sets a new property currentRow I can use in the update and picker delegate to get the cell I want to modify.

var currentRow = 0

I can add this function to the three entry buttons with

currentRow(button: sender)

That sets the current row. Running this, I find I can add a few rows and select the correct index for the row. Now I can use it. There a few places I’ll need it. In each action, I’ve been using the property runStats. Instead of doing a lot of extra refactoring, I’ll change to a local identifier called runStats.

let runStats = statsIntervals.intervals[currentRow]

I add this in to the updateDisplay and pickerDidFinish methods too, so their runStatsare local constants for the cell. I run this and I can change the numbers, they update correctly so I can add two more rows like this:

Rows seem to be working. I try one more , changing the first interval to a 30 second 10:00 min per mile and the second to a 30 second 20 minute per mile walk.

 

That works as expected. I’m having one of those moments where you look at an old picture of yourself and wonder” was that ever me? I’ve changed as a developer. I believe we all do. The code I wrote years ago is so different than the code I write today. No matter how old or long Iv’e been at this, my code writing style is more elegant than ever. Struggles I had years ago don’t happen. There also the change in language to Swift. I tend think more clearly in Swift than I do Objective-C.

Of course that does not mean this app works and I’m ready to ship. In the next installment, I’ll get the units in the intervals to work correctly and solve one of the big issues I haven’t addressed yet — How do the two halves of the app interact?

Steve Lipton is the mind behind makeapppie.com for learning iOS programming. He is the author of several books on iOS programming and development. He is also a LinkedIn Learning and Lynda.com author. His latest courses are Advanced iOS ApplicationDevelopment:Core Motion and Learning Swift Playgrounds Application Development

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