Students will build a simple journal app to practice MVC separation, protocols, master-detail interfaces, table views, and persistence.
Journal is an excellent app to practice basic Cocoa Touch principles and design patterns. Students are encouraged to repeat the building journal regularly until they master the principles and patterns and build journals without a guide.
Students who complete this project independently can:
- Understand basic model-view-controller design and implementation
- Create a custom model object with a memberwise initializer
- Understand, create, and use a shared instance
- Create a model object controller with create, read, update, and delete functions
- Implement the Equatable protocol
- Implement a master-detail interface
- Implement the
UITableViewDataSource
protocol - Understand and implement the
UITextFieldDelegate
protocol to dismiss the keyboard - Create relationship segues in Storyboards
- Understand, use, and implement the 'updateViews' pattern
- Implement 'prepare(for segue: UIStoryboardSegue, sender: Any?)' to configure destination view controllers
- Add data persistence using the Codable protocol and write data to a local file path (URL).
- Upon launch, decode the data returned from the local file path (URL) back into our custom model objects.
Create an Entry model class that will hold title, text, and timestamp properties for each entry.
- Add a new
Entry.swift
file and define a newEntry
class - Add properties for timestamp, title, and body text
- Add a memberwise initializer that takes parameters for each property
- Consider setting a default parameter value for the timestamp.
Create a model object controller called EntryController
that will manage adding, reading, updating, and removing entries. For entry objects in the controller, there should be a consistent source of truth for the entry objects to follow the shared instance design pattern.
- Add a new
EntryController.swift
file and define a newEntryController
class within then class. - Add an entries array property, and set its value to an empty array
- Create a
addEntryWith(title: ...)
function that takes in atitle
, andtext
. It should create a new instance ofEntry
and add it to the entries array - Create a
remove(entry: Entry)
function that removes the entry from the entries array
- There is no 'removeObject' function on arrays. Find the index of the object and then remove the object at that index.
- You will face a compiler error because we have not given the Entry class a way to find equal objects. To resolve the error, implement the Equatable protocol in the next step.
- Create an
update(entry: ...)
function that should take in an existing entry as a parameter and the title and text strings to update the entry. - Create a
shared
property as a shared instance
- Review the syntax for creating shared instance properties
Implement the Equatable protocol for the Entry class. The Equatable protocol helps to check for equality between two variables of a specific class. To ensure that the two objects we are comparing when using this protocol are the same, manually check the values of all the variables - the title, text, and timestamp properties.
- Add the Equatable protocol function in an extension to the bottom of the
Entry.swift
file - Return the result of the comparison between the 'lhs' and 'rhs' parameters by checking the property values on each parameter.
Build a view that lists all journal entries. Use a UITableViewController and implement the UITableViewDataSource functions.
The UITableViewController subclass template comes with a lot of boilerplate and commented code. For readability, please remove all unnecessary boilerplate from the code.
This view will reload the table view each time it appears in order to display newly created entries.
- Add a UITableViewController as the root view controller in Main.storyboard and embed it into a UINavigationController
- Create an
EntryListTableViewController
file as a subclass of UITableViewController. Set the class of the root view controller scene - Implement the UITableViewDataSource functions using the EntryController
entries
array
- Pay attention to the
reuseIdentifier
in the Storyboard scene and thedequeueReusableCell(withIdentifier:for:)
function call
- Set up the cells to display the title of the entry
- Implement the UITableViewDataSource
tableView(_:commit:forRowAt:)
function to enable swipe to delete functionality - Add a UIBarButtonItem to the UINavigationBar.
- 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, including localization and other benefits
Build a view that provides editing and view functionality for a single entry. Use a UITextField to capture the title, a UITextView to capture the body, a UIBarButtonItem to save the new or updated entry, and a UIButton to clear the title and body text areas.
The Detail View should follow the 'updateViews' pattern for updating the view elements with the details of a model object. To follow this pattern, add an 'updateViews' function that checks for a model object. The function updates the view with details from the model object.
- Add an
EntryDetailViewController
file as a subclass of UIViewController and an optionalentry
property to the class - Add a UIViewController scene to Main.storyboard and set the class to
EntryDetailViewController
- Add a UITextField for the entry's title text to the top of the scene, add an outlet to the class file called
titleTextField
. Set the delegate relationship by adopting the UITextFieldDelegate protocol in theEntryDetailViewController
class. - Implement the delegate function
textFieldShouldReturn
and call the resignFirstResponder() method on the titleTextField to dismiss the keyboard. - Add a UITextView for the entry's body text beneath the title text field and add an outlet to the class file
bodyTextView
. - Add a UIButton beneath the body text view and add an IBAction to the class file that clears the text in the titleTextField and bodyTextView.
- Add a UIBarButtonItem to the UINavigationBar as a
Save
System Item and add an IBAction to the class file calledsaveButtonTapped
(You will need to add a segue fromEntryListTableViewController
to see a UINavigationBar on the detail view; and a UINavigationItem to add the UIBarButtonItem to the UINavigationBar, in the next section, this will be covered) - In the
saveButtonTapped
IBAction, check if the optionalentry
property holds an entry. If so, call theupdate(entry: ...)
function in theEntryController
to update the properties of the entry. If not, call theadd(entry: Entry)
function on theEntryController
. After adding a new entry, or updating the existing entry, dismiss the current view. - Add an
updateViews()
function that checks if the optionalentry
property holds an entry. If it does, implement the function to update all view elements that reflect details about the model objectentry
(in this case, the titleTextField and bodyTextView) - Update the
viewDidLoad()
function to callupdateViews()
Add two separate segues from the List View to the Detail View. The segue from the plus button will tell the EntryDetailViewController that it should create a new entry. The segue from a selected cell will tell the EntryDetailViewController that it should display a previously created entry, and save all changes.
- Add a 'show' segue from the Add button to the EntryDetailViewController scene. This segue will not need an identifier since we will not be passing information using this segue.
- Add a 'show' segue from the table view cell to the EntryDetailViewController scene and give the segue an identifier (When naming the identifier, consider that this segue will be used to edit an entry)
- Add a
prepare(for segue: UIStoryboardSegue, sender: Any?)
function to the EntryListTableViewController - Implement the
prepare(for segue: UIStoryboardSegue, sender: Any?)
function. If the identifier is 'showEntry' we will pass the selected entry to the DetailViewController, which will call ourupdateViews()
function
- You will need to capture the selected entry by using the indexPath of the selected cell
- Remember that the
updateViews()
function will update the destination view controller with the entry details
- Implement UITableViewCellEditingStyles to enable swipe to delete entries on the List View Please See: https://developer.apple.com/documentation/uikit/uitableviewdelegate/1614869-tableview?language=objc
Use the Codable protocol to add necessary data persistence to the Journal app. Once the model objects are encoded, save this data to a local file on disk. To access this file, use the URL pointing it. This is a file URL, not a Web URL. It is similar to those seen in Finder ex: path (User/Desktop/Projects/MyProject/myData.json)
Our EntryController
object is the source of truth for entries. We are now adding a layer of persistent storage. We need to update the EntryController
to load entries from persistent storage upon initialization and save the entries to persistent storage when they are created/updated.
- Copy and paste this method into the project. Note that this method returns a URL, which is the URL for the file location where we will be saving our data.
private func fileURL() -> URL {
let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let fileName = "journal.json"
let documentsDirectoryURL = urls[0].appendingPathComponent(fileName)
return documentsDirectoryURL
}
- Write a method called
saveToPersistentStorage()
that will save the current entries array to a file on disk. Implement this function to: - Create an instance of
JSONEncoder
- Call
encode(value: Encodable) throws
on your instance of the JSONEncoder, passing in the array of entries as the Encodable argument. Assign the return of this function to a constant nameddata
. NOTE - The objects in the array need to beCodable
objects. Go back to the Entry class and adopt the Codable protocol. Please see Encoding & Decoding Custom Types: https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types or reference the guided lecture from this morning. - Notice that this function throws; that means that this function will throw an error if it does not work the way it should when called. Functions that throw need to be marked with
try
in front of the function call. Put this call inside a do catch block andcatch
any error that throws. _ If you need to learn about doing catch blocks, review the documentation._ - Call
data.write(to: URL)
This function asks for a URL. We can pass in thefileURL()
as an argument, which will write the data at the URL. Hint - This is also a throwing function. - Call
saveToPersistentStorage()
any time that the list of entries is modified (CRUD functions)
The screenshot below shows how local URLs work. URLs are not just web-based. On the computer, there are local file URLs. Open the finder and right-click to "get info". When done, it will show the location of the folder on the local machine. For example, iCloud Drive / Desktop / Dev Mountain Bank / etc. Local files are separated by components which are forward-slashes. Extensions are . (dots). Images are a good example of extensions such as .jpg or .png
-
Write a method called
loadFromPersistentStorage()
that will load the current data from the file on disk where we saved our entries(data). Implement this function to: -
Create an instance of
JSONDecoder
-
Create a constant called
data
to hold the data that you will get back by callingData(contentsOf:)
. Now pass in thefileURL()
as an argument. (This is a throwing function) -
Call
decode(from:)
on your instance of the JSONDecoder. Assign the return of this function to a constant namedentries
. This function takes in two arguments: a type[Entry].self
, and your instance of data. It will decode the data into an array of Entry. -
Now set self.entries to this array of entries.
-
Call the
loadFromPersistentStorage()
function when theEntryController
is initialized
Run the app; it should now function properly—test for bugs.
- Add support for multiple journals by adding a Journal object that holds entries, a Journal list view that displays Journals, and making the Entry list view display just the entries from the selected journal
- Add support for tags on journals, add functionality to select a tag to display a list of entries with that tag
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.