Implementing ItemCell

We have tests that make sure that configCell(with:) gets called when the cell is prepared. Now, we need tests to make sure that the information is set to the label of ItemCell. You may ask, "What label?", which would be correct, as we also need tests to make sure that ItemCell has labels in order to present the information.

Select the ToDoTests group in the Project Navigator and add a new test case. Call it ItemCellTests. Add the import @testable import ToDo statement and remove the two template test methods.

To be able to present the data on the screen, ItemCell needs labels. We will add the labels in Interface Builder (IB). This means that to test whether the label is set up when the table view cell is loaded, we need to set up the loading in a similar way to how it will be in the app. The table view needs a data source, but we don't want to set up the real data source, because we will then need an item manager. Instead, we will use a fake object to act as the data source.

Add the following code to ItemCellTests.swift but outside of the ItemCellTests class:

extension ItemCellTests { 
  class FakeDataSource: NSObject, UITableViewDataSource { 
     

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

      return 1 
    } 
     
     

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

      return UITableViewCell() 
    } 
  } 
} 

This is the minimal implementation a table-view data source needs. Note that we are returning a plain UITableViewCell. We will see in a minute why this does not matter. Add the following test to ItemCellTests:

func test_HasNameLabel() { 
  let storyboard = UIStoryboard(name: "Main", bundle: nil) 
  let controller = storyboard 
    .instantiateViewController(withIdentifier: "ItemListViewController") 
    as! ItemListViewController 
   

  controller.loadViewIfNeeded()
   

  let tableView = controller.tableView 
  let dataSource = FakeDataSource() 
  tableView?.dataSource = dataSource 
   

  let cell = tableView?.dequeueReusableCell( 
    withIdentifier: "ItemCell", 
    for: IndexPath(row: 0, section: 0)) as! ItemCell 
   

  XCTAssertNotNil(cell.titleLabel) 
} 

This code creates an instance of the View Controller from the storyboard, and it sets an instance of FakeDataSource to its table-view data source. Then, it dequeues a cell from the table view and asserts that this cell has titleLabel. This code does not compile because 'ItemCell' has no member 'titleLabel'. Open ItemCell.swift in Assistant Editor and add the property declaration let titleLabel = UILabel().

Run the tests. All tests pass, but the code is clearly not what we want. First, the label is not set in the storyboard and second, the label is not added to the content view of the cell. This means that when we run the app, the label isn't visible. To drive the implementation, we need a failing test.

Change the assertion in test_HasNameLabel() to the following:

XCTAssertTrue(cell.titleLabel.isDescendant(of: cell.contentView))

With this assertion, we check whether the titleLabel is added to the content view of the cell as a subview.

To make the test pass (and use the storyboard to add the label), replace the property definition let titleLabel = UILabel() with the declaration @IBOutlet var titleLabel: UILabel!.

Open Main.storyboard and add a label to ItemCell as follows:

Open ItemCell.swift in Assistant Editor and hold down the ctrl key while you drag from the Label to the property to connect the two.

Run the tests. Now, all the tests pass.

The item cell also needs to show the location if one is set. Add the following test to ItemCellTests:

func test_HasLocationLabel() { 
  let storyboard = UIStoryboard(name: "Main", bundle: nil) 
  let controller = storyboard 
    .instantiateViewController( 
      withIdentifier:"ItemListViewController") 
    as! ItemListViewController 
   

  controller.loadViewIfNeeded()
   

  let tableView = controller.tableView 
  let dataSource = FakeDataSource() 
  tableView?.dataSource = dataSource 
   

  let cell = tableView?.dequeueReusableCell( 
    withIdentifier: "ItemCell", 
    for: IndexPath(row: 0, section: 0)) as! ItemCell 
   

  XCTAssertTrue(cell.locationLabel.isDescendant(of: cell.contentView))
} 

To make this test pass, we need to perform the same steps as we did for the Title label. Add the @IBOutlet var locationLabel: UILabel! property to ItemCell, add UILabel to the cell in Main.storyboard, and connect the two by control-dragging from IB to the property.

Run the tests. All the tests pass, but there is a lot of duplication in the previous two tests. We need to refactor them. First, add the following properties to ItemCellTests:

var tableView: UITableView! 
let dataSource = FakeDataSource() 
var cell: ItemCell! 

Then, add the following code to the end of setUp():

let storyboard = UIStoryboard(name: "Main", bundle: nil) 
let controller = storyboard 
  .instantiateViewController( 
    withIdentifier: "ItemListViewController") 
  as! ItemListViewController 
 

controller.loadViewIfNeeded()
 

tableView = controller.tableView 
tableView?.dataSource = dataSource 
 

cell = tableView?.dequeueReusableCell( 
  withIdentifier: "ItemCell", 
  for: IndexPath(row: 0, section: 0)) as! ItemCell

Remove the following code from the two test methods:

let storyboard = UIStoryboard(name: "Main", bundle: nil) 
let controller = storyboard 
  .instantiateViewController( 
    withIdentifier: "ItemListViewController") 
  as! ItemListViewController 
 

_ = controller.view 
 

let tableView = controller.tableView 
let dataSource = FakeDataSource() 
tableView?.dataSource = dataSource 
 

let cell = tableView?.dequeueReusableCell( 
    withIdentifier: "ItemCell", 
    for: IndexPath(row: 0, section: 0)) as! ItemCell 

Run the tests to make sure that everything still works.

We need a third label. The steps are exactly the same as those in the last tests. Make the changes yourself (don't forget the test) and call the label dateLabel.

Now that we have the labels in the item cell, we need to fill them with information when the cell is configured. Add the following test to ItemCellTests:

func test_ConfigCell_SetsTitle() { 
  cell.configCell(with: ToDoItem(title: "Foo")) 
 

  XCTAssertEqual(cell.titleLabel.text, "Foo") 
} 

We call configCell(with:) on the dequeued cell from the setUp() method. Run the tests. The last test fails.

To make the test pass, add the following line to configCell(with:):

titleLabel.text = item.title 

Now, all the tests pass again and there is nothing to refactor.

Next, we move on to the date label. Add the following test to ItemCellTests:

func test_ConfigCell_SetsDate() {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM/dd/yyyy"
let date = dateFormatter.date(from: "08/27/2017")
let timestamp = date?.timeIntervalSince1970

cell.configCell(with: ToDoItem(title: "Foo",
timestamp: timestamp))

XCTAssertEqual(cell.dateLabel.text, "08/27/2017")
}

This test first creates a timestamp from a date string and configures the cell with it, and then it asserts whether the text of the date label matches the expectation.

Run the tests. The last test fails because the dateLabel still shows the word Label from when we dragged it into the storyboard scene. To make the test pass, replace configCell(with:) with the following code:

func configCell(with item: ToDoItem) {

titleLabel.text = item.title


if let timestamp = item.timestamp {
let date = Date(timeIntervalSince1970: timestamp)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM/dd/yyyy"


dateLabel.text = dateFormatter.string(from: date)
}
}

Run the tests. All tests pass, but we need to refactor. It is not a good idea to create a date formatter every time configCell(with:) gets called because the date formatter is the same for all cells. To improve the code, add the following property to ItemCell:

lazy var dateFormatter: DateFormatter = { 
  let dateFormatter = DateFormatter() 
  dateFormatter.dateFormat = "MM/dd/yyyy" 
  return dateFormatter 
}() 

The lazy keyword indicates that this property is set the first time it is accessed. Now, you can delete the local definition of the date formatter:

let dateFormatter = DateFormatter() 
dateFormatter.dateFormat = "MM/dd/yyyy"

Run the tests. Everything still works.

The implementation for the locationLabel (with a test first) is left for you as an exercise.

From the screenshots seen in Chapter 2, Planning and Structuring Your Test-Driven iOS App, we know that the title labels of the cells with the checked items were struck through. An item itself doesn't know that it is checked. The state of an item is managed by the item manager. This means that we need a way to put the state of the item into the configCell(with:) method.

Add the following test to check whether the title of the label has been struck through and that the other labels are empty:

func test_Title_WhenItemIsChecked_IsStrokeThrough() { 
  let location = Location(name: "Bar") 
  let item = ToDoItem(title: "Foo", 
                      itemDescription: nil, 
                      timestamp: 1456150025, 
                      location: location) 

   
  cell.configCell(with: item, checked: true) 
   

  let attributedString = NSAttributedString( 
    string: "Foo", 
    attributes: [NSAttributedStringKey.strikethroughStyle: 
      NSUnderlineStyle.styleSingle.rawValue]) 
   

  XCTAssertEqual(cell.titleLabel.attributedText, attributedString) 
  XCTAssertNil(cell.locationLabel.text) 
  XCTAssertNil(cell.dateLabel.text) 
} 

This test looks a bit like the previous one, but the main difference between them is that we call configCell(with:checked:) with an additional argument, and we assert that the attributedText of titleLabel is set to the expected attributed string.

This test does not compile. Replace the method signature of configCell with the following:

func configCell(with item: ToDoItem, checked: Bool = false) { 
  // ... 
}

Open ItemListDataProviderTests.swift and also change the signature of the overridden method in MockItemCell. Run the tests. The last test added fails. To make it pass, replace configCell(with:checked:) with the following code:

 func configCell(with item: ToDoItem,
checked: Bool = false) {

if checked {
let attributedString = NSAttributedString(
string: item.title,
attributes: [NSStrikethroughStyleAttributeName:
NSUnderlineStyle.styleSingle.rawValue])


titleLabel.attributedText = attributedString
locationLabel.text = nil
dateLabel.text = nil
} else {

titleLabel.text = item.title
locationLabel.text = item.location?.name ?? ""

if let timestamp = item.timestamp {
let date = Date(timeIntervalSince1970: timestamp)


dateLabel.text = dateFormatter.string(from: date)
}
}
}

In case checked is true, we set the attributed text to the Title label. Otherwise, we use the code that we had earlier. Run the tests. Everything works and there is nothing to refactor.

For now, we are finished with the to-do item list. In Chapter 6, Putting It All Together, we will connect the list view controller and the data source with the rest of the application.

In the remaining sections of this chapter, we will implement the other two view controllers. We won't go into as much detail as we have thus far because the tests and the implementation are similar to the ones we have already written.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.231.106