Students will build a simple task tracking app to practice project planning, progress tracking, MVC separation, intermediate table view features, and Core Data.
Students who complete this project independently are able to:
- follow a project planning framework to build a development plan
- follow a project planning framework to prioritize and manage project progress
- identify and build a simple navigation view hierarchy
- create model objects that conform to the Equality and NSCoding protocols
- create model object controllers that use NSKeyedArchiver and NSKeyedUnarchiver for data persistence
- add staged data to a model object controller
- implement a master-detail interface
- implement the UITableViewDataSource protocol
- implement a static UITableView
- create a custom UITableViewCell
- write a custom delegate protocol
- use a date picker as a custom input view
- wire up view controllers to model object controllers
- add a Core Data stack to a project
- implement basic data persistence with Core Data
- use NSPredicates to filter search results
Set up a basic List-Detail view hierarchy using a UITableViewController for a TaskListTableViewController and a TaskDetailTableViewController.
- Add a UITableViewController scene that will be used to list tasks
- Embed the scene in a UINavigationController
- Add an Add system bar button item to the navigation bar
- Add a class file
TaskListTableViewController.swift
and assign the scene in the Storyboard - Add a UITableViewController scene that will be used to add and view tasks
- note: We will use a static table view for our Task Detail view, static table views should be used sparingly, but they can be useful for a table view that will never change, such as a basic form.
- Add a segue from the Add button from the first scene to the second scene
- Add a class file
TaskDetailTableViewController.swift
and assign the scene in the Storyboard
Create a Task model class that will hold a title, note, due date, and status for each task.
- Create a
Task.swift
class file and defune a newTask
class - Add properties for name, notes, due, and isComplete
- note: Since a Task can exist without notes or a due date, notes and due are optional properties
- Add a memberwise initializer that takes parameters for each property
- note: Set default parameters for notes, due, and isComplete
- Adopt the NSCoding protocol and add the required
init?(coder aDecoder: NSCoder)
andencodeWithCoder(aCoder: NSCoder)
functions- note: It is best practice to create static internal keys to use in encoding and decoding (ex.
private let NameKey = "name"
)
- note: It is best practice to create static internal keys to use in encoding and decoding (ex.
- Adopt the Equatable protocol and add the required
func
(lhs: Task, rhs: Task) -> Bool``` function
Interesting note about NSObjects, NSDate and Equatable in Swift 2.0:
NSObjects do not use the ==
function to check equality. They use the older isEqual
function. NSObject comes with a default implementation of isEqual
, so for us to actually be able to check equality using these NSObject subclasses, we will need to override the isEqual
function.
isEqual
does not come with a rhs
and lhs
parameters. It comes with the object
parameter that you are checking for equality against self
.
NSDate
comparisons are beyond the scope of this project. For now, do not check the due date isEqual
or ==
methods.
Create a TaskController model object controller that will manage and serve Task objects to the rest of the application. The TaskController will also handle persistence using NSKeyedArchiver and NSKeyedUnarchiver, which will later be replaced with Core Data.
Views in our application want to display completed and incomplete tasks separately. Build the TaskController with a completedTasks and incompleteTasks computed properties that filter a tasks array.
- Create a
TaskController.swift
file and define a newTaskController
class inside. - Add a tasks Array property with an empty default value.
- Add a completedTasks computed Array property that returns only complete tasks.
- note: Use a filter function on the tasks array
- Add an incompleteTasks computed Array property that returns only incomplete tasks.
- note: Use a filter function on the tasks array
- Create a
addTask(task: Task)
function that adds the task parameter to the tasks array- note: At this point, you will need to remove your computed property, as you cannot set values to computed properties.
- Create a
removeTask(task: Task)
function that removes the task from the tasks array- note: There is no 'removeObject' function on arrays. You will need to find the index of the object and then remove the object at that index.
- note: If you face a compiler error, you may need to check that you have properly implented the Equatable protocol for Task objects
- Create a sharedController property as a shared instance.
- note: Review the syntax for creating shared instance properties
Add mock task data to the TaskController. Once there is mock data, teams can serialize work, with some working on the views with visible data and others working on implementing the controller logic. This is a quick way to get objects visible so you can begin building the views.
There are many ways to add mock data to model object controllers. We will do so using a computed property.
- Create a
mockTasks:[Task]
computed property that holds a number of staged Task objects - Initialize a small number of Task objects with varying properties (include at least one 'isComplete' task and one task with a due date)
- When you want mock data, set self.tasks to self.mockTasks in the initializer. Remove it when you no longer want mock data.
- note: If you have not added an initializer, add one.
- note: Unit tests assume mock data is not being used.
At this point, you can go and wire up your list table view to display the complete or incomplete tasks to check your progress on Part One. We will focus on the list and detail views in Part Two.
Add persistence using NSKeyedArchiver and NSKeyedUnarchiver to the TaskController. Archiving is similar to working with NSUserDefaults, but uses NSCoders to serialize and deserialize objects instead of our initWithDictionary
and dictionaryRepresentation
functions. Both are valuable to know and be comfortable with.
NSKeyedArchiver serializes objects and saves them to a file on the device. NSKeyedUnarchiver pulls that file and deserializes the data back into our model objects.
Because of the way that iOS implements security and sandboxing, each application has it's own 'Documents' directory that has a different path each time the application is launched. When you want to write to or read from that directory, you need to first search for the directory, then capture the path as a reference to use where needed.
It is best practice to separate that logic into a separate method that returns the path. Here is an example method:
func filePath(key: String) -> String {
let directorySearchResults = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory,NSSearchPathDomainMask.AllDomainsMask, true)
let documentsPath: AnyObject = directorySearchResults[0]
let entriesPath = documentsPath.stringByAppendingString("/\(key).plist")
return entriesPath
}
This method accepts a string as a key and will return the path to a file in the Documents directory with that name.
- Write a method called
saveToPersistentStorage()
that will save the current tags array to a file using NSKeyedArchiver- note: ``NSKeyedArchiver.archiveRootObject(self.tasks, toFile: self.filePath(TaskKey))``` note: Avoid 'Magic Strings' when using NSKeyedArchiver. Add a 'TaskKey'.
- Write a method called
loadFromPersistentStorage()
that will load saved Task objects and set self.tasks to the results- note: Capture the data using
NSKeyedUnarchiver.unarchiveObjectWithFile(self.filePath(TaskKey))
, unwrap the Optional results and set self.tasks
- note: Capture the data using
- Call the
loadFromPersistentStorage()
function when the TaskCotnroller is initialized - Call the
saveToPersistentStorage()
any time that the list of tasks is modified
- Add support for projects (task parent object), or tags (task child object) to categorize your tasks
- note: Doing this will require extra black diamond work on the next two sections as well
- Create a Unit test that verifies project or tag functionality by converting an instance to and from NSData
- Add support for due date notifications scheduled to fire when the task is due
- Create a Unit test that verifies notification scheduling
Because of the way that we implement persistence and write the tests, you may need to delete the app to start with a clean slate for testing.
- Verifies mockData
- Verifies completedTasks returns only completed tasks
- Verifies incompleteTasks returns only incomplete tasks
- Verifies TaskController persistence
Build a view that lists all tasks. You will use a UITableViewController and implement the UITableViewDataSource functions. You will start with a basic implementation and return to add editing features and custom cells to mark tasks as complete.
You will want this view to reload the table view each time it appears in order to display newly created tasks.
- Implement the UITableViewDataSource functions using the TaskController tasks array
- Set up your cells to display the title of the task
- Add a UIBarButtonItem to the UINavigationBar with the plus symbol
- note: Select 'Add' in the System Item menu from the Identity Inspector to set the button as a plus symbol, these are system bar button items, and include localization and other benefits
Add swipe-to-delete support for deleting tasks from the List View. When committing the editing style, delete the model object from the controller, then delete the cell from the table view.
- Implement the UITableViewDataSource
commitEditingStyle
functions to enable swipe to delete functionality.
Build a view that provides editing and view functionality for a single task. You will use a static table view to create this form. A static table view should only be used when the contents of the table will be identical each time, otherwise, use a dynamic table view with prototype cells.
You will use a UITextField in the first cell to capture the name, a UIDatePicker from tapping the second cell to capture the due date, a UITextView in the third cell to capture notes, and a 'Save' UIBarButtonItem to save the task.
Look at the task detail screenshot or in the solution code. Set up the Storyboard scene with all required user interface elements to appear similarly.
- Update the table view to use static cells
- Create three separate sections, each with one cell
- Change the name of the first section to 'Name', and add a UITextField to the cell with placeholder text
- note: Placeholder text should tell the user what they put in the text field
- Change the name of the second section to 'Due', and add a UITextField to the cell with placeholder text
- Change the name of the third section to 'Notes', and add a text view to the cell
- Resize the UI elements and add automatic constraints so that they fill each cell
Your Detail View should follow the 'updateWith' pattern for updating the view elements with the details of a model object.
- Add an
updateWithTask(task: Task)
function - Implement the 'updateWith' function to update all view elements that reflect details about the model object (in this case, the name text field, the due date text field, and the notes text view)
- note: Dates require some extra work when we try to set them to labels. We'll implement an extension on NSDate using an NSDateFormatter to get a prettier label in the next step.
Dates are a notoriously difficult programming problem. Date creation, formatting, and math are all challenging for beginner programmers. This section will walk you through creating helper functions, setting dates, and using a date picker in place of a keyboard to set a date label.
Because NSDates do not print in a user readable format, Apple includes the NSDateFormatter class to convert dates into strings, and strings back into dates. We will add an extension to NSDate to make a reusable date.stringValue
function that returns a formatted string.
You could place this extension code directly into the view controller that will display the view, but creating an extension allows you to reuse the code throughout the application, and reuse the DateHelpers file in other projects you work on in the future.
- Add a new
DateHelpers.swift
file and define a new extension on NSDate (extension NSDate { }
) - Create a
stringValue() -> String
function that instantiates an NSDateFormatter, sets the dateStyle, and returns a string from the date.
func stringValue() -> String {
let formatter = NSDateFormatter()
formatter.dateStyle = .MediumStyle
return formatter.stringFromDate(self)
}
- Go back to your
updateWithTask
function and use task.due.stringValue to set the text for the due label
UIDatePicker is used to capture date and time information from a user. By setting a UIDatePicker to the inputView of a UITextField, a UIDatePicker will appear in place of the traditional keyboard. You can use a target action, delegate, or IBAction to capture the date that the user selects to a variable.
- Add a UIDatePicker object as a supplementary view to the detail scene
- note: Drag a UIDatePicker object to the outline area of the Storyboard, Interface Builder will drop it directly beneath the First Responder object
- Set the UIDatePicker to Date mode
- Create an IBOutlet from the UIDatePicker supplementary view to the class file named
dueDatePicker
- Create an IBAction from the UIDatePicker supplementary view to the class file named
datePickerValueChanged
- note: Choose UIDatePicker as the sender type so that you do not need to cast the object to get the date off of it
- Create an optional due date placeholder property
dueDateValue
- Implement the action to store the updated date value to
dueDateValue
Dismissing the keyboard can be done in many ways. You can use the textFieldShouldReturn
delegate function on the system keyboard. When using a custom keyboard, you have two common options: add toolbar with a Done button that resigns first responder as the field's input accessory, or add a tap gesture recognizer that does the same.
- Add a UITapGestureRecognizer object to Table View on the Task Detail Scene
- Create an IBAction from the UITapGestureRecognizer named
userTappedView
that resigns first responder on all text fields or text views
You will add two separate segues from the List View to the Detail View. The segue from the plus button will tell the TaskDetailTableViewController
that it should create a new task. The segue from a selected cell will tell the TaskDetailTableViewController
that it should display a previously created task, and save any changes to the same.
- Add a 'show' segue from the Add button to the detail scene and give the segue an identifier
- note: Consider that this segue will be used to add a task when naming the identifier
- Add a 'show' segue from the table view cell to the TaskDetailViewController scene and give the segue an identifier
- note: Consider that this segue will be used to edit a task when naming the identifier
- Add a
prepareForSegue
function to the TaskListTableViewController - Implement the
prepareForSegue
function. Check the identifier of the segue parameter, if the identifier is 'toAddTask' then we will present the destination view controller without passing a task. - Continue implementing the
prepareForSegue
function. If the identifier is 'toViewTask' we will pass the selected task to the DetailViewController by calling ourupdateWithTask(task: Task)
function- note: You will need to capture the selected task by using the indexPath of the selected cell
- note: Remember that the
updateWithTask(task: Task)
function will update the destination view controller with the task details, you may need to force the detail view to draw it's subviews before updating
Build a custom table view cell to display tasks. The cell should display the task name and have a button that acts as a checkmark to display and toggle the completion status of the task.
It is best practice to make table view cells reusable between apps. As a result, you will build a ButtonTableViewCell
rather than a TaskTableViewCell
that can be reused any time you want a cell with a button. You will add an extension to the ButtonTableViewCell
for updating the view with a Task.
- Add a new
ButtonTableViewCell.swift
as a subclass of UITableViewCell - Assign the new class to the prototype cell on the Task List Scene in
Main.storyboard
- Design the prototype cell with a label on the left and a square button on the right margin
- note: If using a stack view, constrain the aspect ratio of the button to 1:1 to force the button into a square that gives the remainder of the space to the label
- note: Use the image edge inset to shrink the image to not fill the entire height of the content view
- Remove text from the button, but add a image of an empty checkbox
- note: Use the 'complete' and 'incomplete' image assets included in the project
- Create an IBOutlet for the label named
primaryLabel
- Create an IBOutlet for the button named
button
- Create an IBAction for the button named
buttonTapped
which you will implement using a custom protocol in the next step
Implement the 'updateWith' pattern in an extension on the ButtonTableViewCell
class.
- Add an
updateButton(isComplete: Bool)
function that updates the button's image to the desired image based on the isComplete Bool - Add an extension to
ButtonTableViewCell
at the bottom of the class file - Add a function
updateWithTask(task: Task)
that updates the label to the name of the task, and calls theupdateButton(isComplete: Bool)
function to update the image - Update the
cellForRowAtIndexPath
to callupdateWithTask(task: Task)
instead of setting the text label directly
Write a protocol for the ButtonTableViewCell
to delegate handling a button tap to the TaskListTableViewController
, adopt the protocol, and use the delegate method to mark the task as complete and reload the cell.
- Add a protocol named
ButtonTableViewCellDelegate
to the bottom of the class file - Define a required
buttonCellButtonTapped(sender: ButtonTableViewCell)
function - Add an optional delegate property on the ButtonTableViewCell, require the delegate to have adopted the delegate protocol
- note:
var delegate: ButtonTableViewCellDelegate?
- note:
- Update the
buttonTapped
IBAction to check if a delegate is assigned, and if so, call the delegate protocol function - Adopt the protocol in the
TaskListTableViewController
class - Implement the
buttonCellButtonTapped
delegate method to capture the Task as a variable, toggle task.isComplete, save to persistent storage, and reload the table view
- Add a segmented control as the title view that toggles whether the table view should display complete or incomplete tasks
- Add support for entering 'Editing' mode on a table view and add a cell that allows you to insert new tasks
- Add automatic resizing to the table view cell with the Notes text view
- Update the settings for the checkbox images to inherit the tint color of the button
Because of the way that we implement persistence and write the tests, you may need to delete the app to start with a clean slate for testing.
- Validates task lifecycle (add, modify, delete)
- Validates task lifecycle (add, modify, mark complete)
- Validates Detail View updating with task data
- Validates date capture using date picker
- Validates dismissing keyboard via gesture recognizer
You will use Core Data to add more advanced data persistence to the Task app. You will refactor the Task model object and the Task Controller to work with a Core Data stack. The View Controllers should work without any modification.
Add a simple Stack to your application to start working with Core Data. You will build your Data Model and NSManagedObject subclass objects. Then you will add a Stack class that will initialize your persistent store, coordinator, and managed object context.
- Import the Core Data
Stack.swift
template available on Github- note: Review how this file works, and what it does for you each time you work with it.
- Create a new Data Model template file (File -> New -> iOS Core Data -> Data Model), use the app name
- Add a New Entity called Task with the same properties and types as our old Task object Entity with properties
- Use the Data Model Inspector to set notes and due to optional values, set isComplete with a default value of false
- Assign a Class name of Task, this is what Xcode will use to create your NSManagedObject and CoreDataProperties files
- Delete the
Task.swift
file we created earlier to make room for the Xcode generated NSManagedObject - Create Managed Object Subclass
- note: Consider the purpose of the two different files you get
- note: Verify that name and isComplete are required properties, and notes and due are optional properties
Now you need to add a Convenience Initializer to your Task.swift
file that matches our old initializer for the Task object. NSManagedObjects have a designated initializer called init(entity: entity, insertIntoManagedObjectContext: context)
that gets called by the NSEntityDescription.entityForName("Task", inManagedObjectContext: context)
function that is traditionally used to create Managed Objects. You will write a convenience initializer that uses those two designated function calls and then set properties on the Task.
- Add a Convenience Initializer to the
Task.swift
file that matches the old Task initializer- note: You can optionally add a 'managedObjectContext' parameter, but for our app we only have one, and we can set it to a default parameter value of
Stack.sharedStack.managedObjectContext
)
- note: You can optionally add a 'managedObjectContext' parameter, but for our app we only have one, and we can set it to a default parameter value of
convenience init(name: String, notes: String? = nil, due: NSDate? = nil, context: NSManagedObjectContext = Stack.sharedStack.managedObjectContext) {
// there is no graceful way to respond to a failure on NSEntityDescription.entityForName, force unwrapping and forcing a crash is the desired behavior for this app
// designated initializer
let entity = NSEntityDescription.entityForName("Task", inManagedObjectContext: context)!
self.init(entity: entity, insertIntoManagedObjectContext: context)
// set properties here
}
Following proper MVC principles shields you from refactoring major portions of the application, however, there are a few changes to make to TaskController
and other classes to adjust for how Core Data works.
- Refactor the
.tasks
array to be a computed property - In the computed property, instantiate an NSFetchRequest for 'Task' entities
- Execute and return the results of the fetch request
- You can now remove
loadFromPersistentStorage
because we now do a fetch request each time the array of tasks is accessed
Core Data does not store Boolean properties, as a result, NSManagedObjects store Booleans as NSNumber properties. NSNumber comes with a helper function .boolValue
that is useful for converting the NSNumber into a Bool. Refactor your code that used the .isComplete
property to now use .isComplete.boolValue
- Update isComplete to
isComplete.boolValue
everywhere it was being used. The compiler should direct you to specific cases
- Refactor the 'incompleteTasks' and 'completedTasks' arrays to use an NSFetchRequest with an NSPredicate to return the correct results
- Implement a Fetched Results Controller for the Table View DataSource
- Validates same assumptions as Part 1 and Part 2
Please refer to CONTRIBUTING.md.
© DevMountain LLC, 2015. Unauthorized use and/or duplication of this material without express and written permission from DevMountain, LLC is strictly prohibited. Excerpts and links may be used, provided that full and clear credit is given to DevMountain with appropriate and specific direction to the original content.