This Old App: Container Views and Table Cell Actions

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 my last installment I added a custom table view cell. In this installment, I’ll get the root view and the table view sending models to each other. I’ll also also get the buttons in the container view to communicate with the root view.

I’ve gotten the cells working, but only when the model is completely internal to the table view controller. I want to send data from the table to the three root variables

The table view is in a container view. How do I send data to something in a container view?

Container views are subviews that act like child view controllers. You use a special segue to do this:

This is an embed segue. You use it a like any other segue. I’ll set an identifier for the segue

I’ll add a prepare:forSegue: to that

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

    if segue.identifier == "IntervalTable"{

      let vc = segue.destination as! IntervalTableViewController

      vc.runStats = self.runStats


    }
}

I’ll remove the setting of the runStats in the table view, and set the number of rows back to 1 for testing.

If I change the pace on top to 11:00, the bottom doesn’t change however.

The segue is static, only launching once on load. Instead, the tableview should update when changes to the root happen. I’ll add a private variable to the class to act as a pointer to the table:

private var tableViewController = IntervalTableViewController()

Then change the prepare:forSegue to use the controller:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

    if segue.identifier == "IntervalTable"{

      tableViewController = segue.destination as! IntervalTableViewController

      tableViewController.runStats = self.runStats

     

    }

  }

When I update the display for the root, I’ll also refresh the table:

//update the table

    tableViewController.tableView.reloadData()

Now I try 11:00, and both stats update.

I can’t check the other way yet, because the buttons don’t work.

Adding Actions to the Tableview Cell

Ideally we should be able to copy and paste the code from the Actions from the root controller into the tableview cell. Unfortunately, it’s not that easy. There’s three issues that prevent us from doing that.

The first is getting the picker using the storyboard identifier. Secondly the colors(pickerVC: from:sender) doesn’t exist in the cell. Finally, presenting the cell doesn’t work here. The table view cell is not a subclass of TableViewController, nor is it inheriting the Colors method. The colors method is the low hanging fruit here, and I’ll fix that first. Currently it is this.

 func colors(pickerVC:PickerViewController, from button:UIButton){

    if let backgroundColor = view.backgroundColor{

      pickerVC.backgroundColor = backgroundColor

    }

    if let toolBarColor = button.superview?.backgroundColor{

      pickerVC.toolBarColor = toolBarColor

    }

    if let titleColor = button.titleColor(for: .normal){

      pickerVC.textColor = titleColor

    }

  }


I make this part of the picker with a few changes. Instead of pickerVC as a parameter, I can use a view and a button.

func colors(view:UIView, button:UIButton){

    if let backgroundColor = view.backgroundColor{

      self.view.backgroundColor = backgroundColor

    }

    if let toolBarColor = button.superview?.backgroundColor{

      self.toolBarColor = toolBarColor

    }

    if let titleColor = button.titleColor(for: .normal){

      self.textColor = titleColor

    }

  }


I’ll comment out code for the moment and make a change to the timeEntry in the root to see if this works.

//colors(pickerVC: vc, from: sender)

      vc.colors(view: view, button: sender)

And it does:

I’ll change everything to this. It makes better sense to me that the picker controls it own colors than the view controller now that I think of it.

Actions in a Custom Tableview Cell

There’s a school of thought I didn’t follow up to now, but I see the logic at exactly this point: The cell has contents and outlets, but the actions are in the table view controller. Apple forces you to do this in WatchOS for example. The cell is too limited to do everything it needs to do. There’s a price for this: I need to know which cell I’m working with. I’ll start with one cell then deal with the tracking problem second.

I’ll move the actions from the cell the the tableview like this. I’ll cut the actions in the storyboard from the tableview cell, then copy my actions to the table view controller. I’ll clean the whole thing with Command-Shift-K , the reconnect to the table view controller’s copy of those actions. All the errors I had disappear, but there is a new one: the picker delegate. I’ll adopt the PickerViewControllerDelegate, and copy the pickerDidFinish method from RootViewController as a delegate method for the table view. I’ll change the updateDisplay method to tableView.reloadData.

I’ll now test if the buttons work. I open up the app and click the 26.2 distance. I get a picker. The colors are working correctly, since I have a white background for the table view, and a blue numbers for the button.

Changing the picker to 13.10 I get on the table view

Though nothing updates but the button. I can do that easily, after I implement the button strategy I came up in the last installment. When I was thinking about intervals, you can only lock distance or time. Pace is never locked for an interval. For intervals I only need a toggle. Instead of the code I had before that checks the lock if you can change a value, everything will be allowed to change and when you hit either distance or time, the other gets the lock. For time this look like this:

@IBAction func timeEntry(_ sender: UIButton) {

    runStats.locked = .distance

 …

}

Distance gets the same treatment:

@IBAction func distanceEntry(_ sender: UIButton) {

    runStats.locked = .time

…

}

In the tableViewController’s pickerDidFinish, I can recalculate before I reload the data.

runStats.recalculate()

tableView.reloadData()

I’ll run, and just like before, I’ll change the distance to a half marathon of 13.1

The time drops to half the original time. If I change the miles back to 26.2, then change the time to 2:11, I get the same result. If I change the pace to 13:30, then I get this:

The time is the last thing locked, so the distance changes. Change the distance to 13.10, and the time recalculates.

I’ll use a user interface arrangement to makes this a little simpler. If we always add pace first, then this display will make more sense. By placing Pace first we encourage inputting the pace then either distance or time. I do some quick changing of the layout.

This is a lot better. A behavioral change is far more elegant than a code change.

The Container View Delegate 

With the cell and table working, Now I want to get that value back to the root. For that I’ll use a delegate. In the IntervalTableViewController.swift file, I add the protocol

protocol IntervalTableViewControllerDelegate {

  func didChangeStats(stat:RunStats, controller:IntervalTableViewController)

}

I add the property to the IntervalViewController

var delegate:IntervalTableViewControllerDelegate! = nil

I’m getting so many things to update, I’ll make an updateDisplay method

func updateDisplay(){

    runStats.recalculate()

    tableView.reloadData()

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

  }

I’ll replace the two lines in pickerDidFinish with the update display then adopt this protocol in the RootViewController

class RootViewController: UIViewController,PickerViewControllerDelegate,IntervalTableViewControllerDelegate

and make the required function

  func didChangeStats(stat: RunStats, controller: IntervalTableViewController) {

    self.runStats = stat

    updateDisplay()

  }

with a slight change so runStats is a var instead of let.

var runStats = RunStats()

Set the delegate to self in the prepare:forSegue:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

    if segue.identifier == "IntervalTable"{

      tableViewController = segue.destination as! IntervalTableViewController

      tableViewController.runStats = self.runStats

      tableViewController.delegate = self

     

    }

  }


I try setting to a half marathon, with wonderful results:

I’ve gotten the cell to show values from the main display, and for the cell to send back values to the main display. Along the way required a few change to how I worked with the cell and the table. Actions for a cell belong in the table view if you need the power of view controller, like we do here. We’ve been using one cell so far. In the next installment, we’ll go to multiple cells.

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