We start the implementation of DetailViewController
with the creation of a test case. Select the ToDoTests group in Project Navigator, and go to iOS | Source | Unit Test Case Class. Let's name it DetailViewControllerTests
, and select the Controller
folder as the destination location. Import the @testable import ToDo
main module and delete the two template test methods.
Going by the screenshots we've seen in Chapter 2, Planning and Structuring Your Test-Driven iOS App, we know that DetailViewController
needs a map view, four labels, and a button. Here, we will only show the TDD process for one label and the button. Add the following code to DetailViewControllerTests
:
func test_HasTitleLabel() { let storyboard = UIStoryboard(name: "Main", bundle: nil) let sut = storyboard.instantiateViewControllerWithIdentifier( "DetailViewController") as! DetailViewController }
At this point, we have to stop writing the test because there is no DetailViewController
yet. Select the ToDo group in Project Navigator and add an iOS | Source | Cocoa Touch Class with the name DetailViewController
. Choose the Controller
folder as destination location. As we did earlier, remove everything from the class except the minimal class definition:
import UIKit class DetailViewController: UIViewController { }
Now, add the following to the end of test_HasTitleLabel()
:
_ = sut.view XCTAssertNotNil(sut.titleLabel)
The test does not compile because there is no titleLabel
in DetailViewController
. Add the following property to DetailViewController
:
@IBOutlet weak var titleLabel: UILabel!
Run the tests. The last test fails because the storyboard doesn't contain a view controller with identifier 'DetailViewController'
. Let's fix this. Open Main.storyboard
and add a View Controller to it. In Identity Inspector, change its Class and the Storyboard ID to DetailViewController
.
Run the tests again. It still fails because the titleLabel
property is nil
. Again, open Main.storyboard
and add a label to the View Controller scene. In Assistant Editor, open DetailViewController
, and connect the label in the storyboard to the outlet by holding down the control key while you drag from the label to the outlet.
Run the tests. Now, all the tests pass.
We already know that we need tests for the other labels and map view. So, let's put the setup code into setUp()
. First, add the property var sut: DetailViewController!
to DetailViewControllerTests
and add the following code to setUp()
:
let storyboard = UIStoryboard(name: "Main", bundle: nil) sut = storyboard.instantiateViewControllerWithIdentifier( "DetailViewController") as! DetailViewController _ = sut.view
Replace test_HasTitleLabel()
with the following:
func test_HasTitleLabel() { XCTAssertNotNil(sut.titleLabel) }
Run the tests again to make sure we didn't break anything during refactoring. Everything still works.
Add the remaining three labels using TDD.
For the map view, we need to add the MapKit framework. Select the project in Project Navigator, and switch on Maps in the Capabilities tab:
Add the following test to DetailViewControllerTests
:
func test_HasMapView() { XCTAssertNotNil(sut.mapView) }
To make the test pass, first import MapKit
in DetailViewController
:
import MapKit
Then, add the outlet @IBOutlet weak var mapView: MKMapView!
and a map view element in the storyboard and connect the two (by control-dragging). Run the tests to make sure everything works.
When presenting DetailViewController
, ItemListViewController
needs to be able to set the item to be shown. As the user will be able to check items in the details view, we will pass the item manager plus the selected index to DetailViewController
. And we will assume that the details can only be presented for unchecked items. This makes sense for the app because checked items are not that important anymore for the user. If we later decide that we also want to show the details for checked items, we can still add this feature.
We will now write a test that ensures that we can pass the data to DetailViewController
, and the information is shown in the labels. Add the following code to DetailViewControllerTests
:
func testSettingItemInfo_SetsTextsToLabels() { let coordinate = CLLocationCoordinate2D(latitude: 51.2277, longitude: 6.7735) let itemManager = ItemManager() itemManager.addItem(ToDoItem(title: "The title", itemDescription: "The description", timestamp: 1456150025, location: Location(name: "Home", coordinate: coordinate))) sut.itemInfo = (itemManager, 0) }
We have two errors in this code already. Firstly, CLLocationCoordinate2D(latitude:longitude:)
is defined in Core Location. So, we need to add this module to the test code. Add the following import statement right below the existing import statements:
import CoreLocation
Secondly, 'DetailViewController' has no member 'itemInfo'
. Add the following property declaration to DetailViewController
:
var itemInfo: (ItemManager, Int)?
With this change, there are no errors from the static analyzer anymore. Let's move on.
We will fill the labels with the information from the to-do item in viewWillAppear(_:)
. Because of this, we need to trigger the call of that method in the test. It is not recommended that you call this method directly. Instead, you can ask the View Controller to begin and end the appearance transition. Add the following code to testSettingItemInfo_SetsT
extsToLabels()
:
sut.beginAppearanceTransition(true, animated: true) sut.endAppearanceTransition() XCTAssertEqual(sut.titleLabel.text, "The title") XCTAssertEqual(sut.dateLabel.text, "02/22/2016") XCTAssertEqual(sut.locationLabel.text, "Home") XCTAssertEqual(sut.descriptionLabel.text, "The description") XCTAssertEqualWithAccuracy(sut.mapView.centerCoordinate.latitude, coordinate.latitude, accuracy: 0.001)
With beginAppearanceTransition(_:animated:)
and endAppearanceTransition()
, we trigger the call of viewWillAppear(_:)
(and viewDidAppear(_:)
and similar methods for the presentation of the view hierarchy). Then, we assert that the information from the to-do item is set to the labels and map view of DetailViewController
. Run the tests. The last test fails because we haven't implemented viewWillAppear(_:)
yet. Open DetailViewController
and add the implementation:
override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) guard let itemInfo = itemInfo else { return } let item = itemInfo.0.itemAtIndex(itemInfo.1) titleLabel.text = item.title locationLabel.text = item.location?.name descriptionLabel.text = item.itemDescription if let timestamp = item.timestamp { let date = NSDate(timeIntervalSince1970: timestamp) dateLabel.text = dateFormatter.stringFromDate(date) } if let coordinate = item.location?.coordinate { let region = MKCoordinateRegionMakeWithDistance(coordinate, 100, 100) mapView.region = region } }
Add the definition of the date formatter below the existing properties:
let dateFormatter: NSDateFormatter = { let dateFormatter = NSDateFormatter() dateFormatter.dateFormat = "MM/dd/yyyy" return dateFormatter }()
Run the tests. All the tests pass again and there is nothing to refactor.
Next, we need to implement the Check button. When the user taps the Check button, the item should be checked in the item manager. Add the following test to DetailViewControllerTests
:
func testCheckItem_ChecksItemInItemManager() { let itemManager = ItemManager() itemManager.addItem(ToDoItem(title: "The title")) sut.itemInfo = (itemManager, 0) sut.checkItem() XCTAssertEqual(itemManager.toDoCount, 0) XCTAssertEqual(itemManager.doneCount, 1) }
This test does not compile because there is no checkItem()
method in DetailViewController
. Add the minimal implementation to make the test compile:
func checkItem() { }
Now, the test compiles but it fails because the method does nothing. To make the test pass, add the following code to checkItem()
:
if let itemInfo = itemInfo { itemInfo.0.checkItemAtIndex(itemInfo.1) }
Run the tests. All the tests pass and there is nothing to refactor. Next, we need to implement InputViewController
.