Showing the input view

The user should be able to add an item to the list view. As shown in the mockups in Chapter 2, Planning and Structuring Your Test-Driven iOS App, there should be an Add button in the navigation bar that presents the input view controller. We will add the following tests to ItemListViewControllerTests because these are tests about ItemListViewController.

Open ItemListViewControllerTests and add this test:

func test_ItemListViewController_HasAddBarButtonWithSelfAsTarget() { 
  let target = sut.navigationItem.rightBarButtonItem?.target 
  XCTAssertEqual(target as? UIViewController, sut) 
} 

To make this test pass, we need to add a bar button item to the item list view controller. Open Main.storyboard, drag a Bar Button Item to the navigation bar of the item list view controller, and set the value of System Item to Add, as shown in the following screenshot:

Open ItemListViewController in the Assistant Editor and control + drag from the button to below viewDidLoad(), as shown in the following screenshot:

Set the value of Connection to Action, Name to addItem, and Type to UIBarButtonItem.

Run the tests again. The tests pass and there is nothing to refactor.

Next, we want to make sure that the input view controller is presented when the user taps the Add button. Add the following test to ItemListViewControllerTests:

func test_AddItem_PresentsAddItemViewController() {

   
  XCTAssertNil(sut.presentedViewController)

   
  guard let addButton = sut.navigationItem.rightBarButtonItem else 
  { XCTFail(); return } 
  guard let action = addButton.action else { XCTFail(); return }

  
  sut.performSelector(onMainThread: action, 
                      with: addButton, 
                      waitUntilDone: true)

  
  XCTAssertNotNil(sut.presentedViewController) 
  XCTAssertTrue(sut.presentedViewController is InputViewController) 
} 

Before we do anything in the test, we make sure that sut does not present a view controller on the screen. Then, we get a reference to the Add button and perform its selector on sut. This makes sense because, from the previous test, we know that sut is the target for this button. Run the test to make sure it fails.

To make the test pass, add the following line to the addItem method:

present(InputViewController(), 
        animated: true, 
        completion: nil) 

Run the test. It still fails. To figure out what is going on, navigate to View | Debug Area | Activate Console. You should see a line with information similar to this:

Warning: Attempt to present <ToDo.InputViewController: 0x7ff2fc75bd90> on <ToDo.ItemListViewController: 0x7ff2fc75a420> whose view is not in the window hierarchy!

The reason for this warning is that we have just instantiated the view controller, but it is not shown anywhere. It is only possible to present a view controller from another view controller whose view is in the view hierarchy. When the app is running outside of the test, this is not an issue because if the user can tap the Add button, the item list view controller must be visible on the screen and therefore its view has to be in the view hierarchy. So, we need to figure out how to write a test for this.

In fact, it is quite easy. We can add the view to the view hierarchy by setting the view controller to the rootViewController property of the key window. Add the following line in test_AddItem_PresentsAddItemViewController() right below the guard statements:

UIApplication.shared.keyWindow?.rootViewController = sut

Run the tests again. Now, all the tests pass, but the code looks strange. We instantiate an instance of InputViewController using its initializer. This bypasses the storyboard. As a result, the outlet connections we created in Chapter 4, A Test-Driven View Controller, are all nil. This means that we wouldn't be able to put in the data for the to-do item we want to add.

So, we need another test to make sure that the implementation code instantiates the input view controller instance using the storyboard. Add the following code at the end of test_AddItem_PresentsAddItemViewController():

let inputViewController = 
  sut.presentedViewController as! InputViewController 
XCTAssertNotNil(inputViewController.titleTextField) 

Run the test to make sure it is red. To make the test pass, replace the contents of addItem(_:) with the following code:

@IBAction func addItem(_ sender: AnyObject) { 
  if let nextViewController = 
    storyboard?.instantiateViewController( 
      withIdentifier: "InputViewController") 
      as? InputViewController {

     
    present(nextViewController, animated: true, completion: nil) 
  } 
} 

This code instantiates an instance of InputViewController from the storyboard and presents it on the screen. Run the tests. All the tests pass.

To be able to add items to the list, ItemListViewController and InputViewController need to share the same item manager. This is possible because ItemManager is a class and therefore both view controllers can hold a reference to the same instance. If we had used struct instead, adding an item in InputViewController would not have changed the item manager referenced by ItemListViewController.

Let's write a test to make sure that both view controllers refer to the same object. Add the following test to ItemListViewControllerTests:

func testItemListVC_SharesItemManagerWithInputVC() {

   
  guard let addButton = sut.navigationItem.rightBarButtonItem else 
  { XCTFail(); return } 
  guard let action = addButton.action else { XCTFail(); return } 
  UIApplication.shared.keyWindow?.rootViewController = sut

   
  sut.performSelector(onMainThread: action, 
                      with: addButton, 
                      waitUntilDone: true)

   
  guard let inputViewController = 
    sut.presentedViewController as? InputViewController else 
  { XCTFail(); return } 
  guard let inputItemManager = inputViewController.itemManager else 
  { XCTFail(); return } 
  XCTAssertTrue(sut.itemManager === inputItemManager) 
} 

The first part of the test is similar to the earlier test. After presenting the input view controller on the screen, we assert that itemManager in inputViewControler refers to the same object as sut.

This test does not compile because Value of type 'ItemListViewController' has no member 'itemManger'. Add the following property to make it compile:

let itemManager = ItemManager()

Run the test. It compiles but fails because itemManager of inputViewController is nil. Add the following line in addItem(_:) right before the next view controller is presented:

nextViewController.itemManager = ItemManager()

Run the test. It still fails, but this time it's because the item manager of sut and input view controller do not refer to the same object. Replace the line you just added with this one:

nextViewController.itemManager = itemManager

Run all the tests. All the tests pass.

If you look at the last two tests, there is a lot of duplicated code. The tests need refactoring. This is left as an exercise for you. You should be able to extract the duplicated code with the knowledge you have gained so far.

Now, let's check whether we can add a to-do item to the list. Build and run the app. Tap the plus (+) button and put a title into the text field connected to the titleTextField property. Tap the Save button (the one that is connected to the save action). Nothing happens. The reason for this is that we did not add the code to dismiss the view controller when the Save button was tapped. We need a test for this.

Open InputViewControllerTests.swift and add the following definition of a mock class after the other mock classes:

class MockInputViewController : InputViewController {

   
  var dismissGotCalled = false
 
   
  override func dismiss(animated flag: Bool, 
                        completion: (() -> Void)? = nil) {

     
    dismissGotCalled = true 
  } 
} 

The mock class is a subclass of InputViewController. The correct term for such a mock is partial mock because it only mocks parts of the behavior of its super class. With this in place, we can write the test:

func testSave_DismissesViewController() { 
  let mockInputViewController = MockInputViewController() 
  mockInputViewController.titleTextField = UITextField() 
  mockInputViewController.dateTextField = UITextField() 
  mockInputViewController.locationTextField = UITextField() 
  mockInputViewController.addressTextField = UITextField() 
  mockInputViewController.descriptionTextField = UITextField() 
  mockInputViewController.titleTextField.text = "Test Title"

  
  mockInputViewController.save()

   
  XCTAssertTrue(mockInputViewController.dismissGotCalled) 
} 

As we do not instantiate from the storyboard, we need to set the text fields in the test; otherwise, the test will crash because it will try to access text fields that are nil. After this, we set a test title to the title text field and call save. This should dismiss the view controller.

Run the test. It fails. To make it pass is quite easy. Add the following line at the end of save():

dismiss(animated: true) 

Now, run all the tests. All the tests pass.

Let's take a look at what the app looks like now. Build and run the app in the simulator, tap the Add button, put in a title, and hit Save. The input view controller is dismissed but no item is added to the list. There are two problems concerning this micro feature. First, the item manager defined in ItemListViewController is not shared with the data provider. Second, after an item has been added to the list, we need to tell the table view to reload its data.

Let's write a test for the first problem. Add the following test to ItemListViewController:

func test_ViewDidLoad_SetsItemManagerToDataProvider() { 
  XCTAssertTrue(sut.itemManager === sut.dataProvider.itemManager) 
} 

This test does not compile because the data provider is of the type (UITableViewDataSource & UITableViewDelegate)!. The compiler cannot know that it also has an itemManager property. To fix this, add the following protocol to ItemDataProvider.swift outside of the class definition:

@objc protocol ItemManagerSettable { 
    var itemManager: ItemManager? { get set } 
} 

Now, the static analyzer tells us that this property cannot be a member of an @objc protocol because its type cannot be represented in Objective-C. But, we need to declare the protocol to be @objc because we've set the data provider from the storyboard. The solution is to make ItemManager a subclass of NSObject:

class ItemManager: NSObject { 
    // .... 
} 

Now, we can make ItemListDataProvider conform to ItemManagerSettable as follows:

class ItemListDataProvider: NSObject, UITableViewDataSource, UITableViewDelegate, ItemManagerSettable { 
    // .... 
} 

We can finally add the protocol in the declaration of the data provider in ItemListViewController:

@IBOutlet var dataProvider: (UITableViewDataSource & UITableViewDelegate & ItemManagerSettable)! 

Run the test. Finally, the test compiles but it fails. To make it pass, add the following line at the end of viewDidLoad() in ItemListViewController:

dataProvider.itemManager = itemManager

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

On to the next problem: we need to make sure that the table view is reloaded when an item is added to the item manager. A perfect place for the reload is viewWillAppear(_:). As an exercise, add this test to ItemListViewControllerTests. You may need a mock for the table view to register when reloadData() is called. A reminder: to trigger viewWillAppear(_:), do this in your test:

sut.beginAppearanceTransition(true, animated: true) 
sut.endAppearanceTransition() 

Write the test as an exercise.

To make the test pass, add the following code to ItemListViewController:

override func viewWillAppear(_ animated: Bool) { 
    super.viewWillAppear(animated)

     
    tableView.reloadData() 
} 

Finally, build and run the app again and add an item to the list. You should see something like this:

If adding a to-do item doesn't work when you run the app, make sure that you have implemented the else path in add() no location is added to the location text field. It should look like this:

let item = ToDoItem(title: titleString, 
                    itemDescription: descriptionString, 
                    timestamp: date?.timeIntervalSince1970, 
                    location: nil) 
self.itemManager?.add(item)
..................Content has been hidden....................

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