Students will build a simple journal app to practice MVC separation, segues, table views, and persistence.
Journal is an excellent app to practice basic Cocoa Touch principles and design patterns. Students are encouraged to repeat building journal regularly until they master the principles and patterns, eventually being able to build journal without the help of 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 and delete functions
- Implement the Equatable protocol
- Understand and implement the
UITextFieldDelegate
protocol to dismiss the keyboard - Create relationship segues in Storyboards
- Implement 'prepare(for segue: UIStoryboardSegue, sender: Any?)' to configure destination view controllers
- Understand, use, and implement the 'updateViews' pattern
- Save and load data using JSON Persistence and the Codable protocol
- Be able to use multiple models in the mvc design pattern
- Be able to refactor an application to reference a new source of truth
- Implement an update function on the model controller to update existing data
- Make sure you have forked and cloned this project
- Once you have navigated into this project using terminal (hint: cd), switch over to the starter branch (hint: git checkout starter) and open the project
- Create a file structure to organize your code (ex. Resources, Views, etc...). Don't forget to fix the file path for your info.plist
- Note: When using git, it is best to not create a folder (ex. Model Controller) until you have a file to put in the folder. Git does not like tracking empty folders. You can use placeholder .swift files, if you would like (ex: placeholderViewController.swift)
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 title, body and timestamp (hint: timestamp should NOT be a String or an Int).
- 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 creating and deleting entries.
- Add a new
EntryController.swift
file and define a newEntryController
class. - Create a
shared
property as a shared instance (hint: remember, shared instances require an important keyword at the beginning of the line of code). - Add an
entries
array property, and set its value to an empty array ofEntry
. - Create a
createEntryWith(title: ...)
function that takes in atitle
andbody
. It should create a new instance ofEntry
and add it to theentries
array. - Create a
delete(entry: Entry)
function that removes an entry from the entries array
- There is no 'removeObject' function on arrays. You will need to first 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.
Implement the Equatable protocol for the Entry class. The Equatable protocol helps to check for equality between variables of a specific class. To ensure that the two objects we are comparing are the same, we will need to make sure the values of all the variables (title, body, and timestamp) are the same.
- Conform to the Equatable protocol in an extension at the bottom of the
Entry.swift
file (outside of the Entry class). This will prompt you with an error - use the fix button to add the necessary protocol stub (function). - Return the result of the comparison between the 'lhs' and 'rhs' parameters by checking the property values on each parameter.
- If you have not already, now go back to your EntryController and finish building out the delete function.
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 in your Main.storyboard to be anEntryListTableViewController
. - Implement the UITableViewDataSource functions, using the EntryController
entries
array as your source of truth.
- 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 (hint: this will need to be done in the cellForRowAt method). Do not forget to set your cell type to 'basic' on your view controller scene.
- 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 dropdown, on the Identity Inspector, to set the button as a plus symbol. Don't do anything further with this button, we will take care of it later.
If you run your application now, you should see a tableView with nothing on it. You should also see a plus button in the top right corner that, when tapped, does nothing. While this is correct, it doesn't really give us a way of testing out our code. At the moment, we still have a bit more to implement before we can test anything. This is where mock data becomes useful. In your viewDidLoad lifecycle method, after super.viewDidLoad(), add the following code EntryController.shared.createEntryWith(title: "Test Title", body: "Test Body")
, followed by tableView.reloadData()
Re-run your app. You should now see an entry on your tableView with the title "Test Title". Clicking on it won't do anything yet, however, you should be able to swipe to delete. If you can see "Test Title" and are able to delete it, your code is in a good place and you can move on. If not, spend 20 minutes to try and work it out. If you cannot solve your problem in 20 minutes, post a message in the queue channel on discord.
Once you have everything working, make sure to remove the two lines of code we added to the viewDidLoad lifecycle method.
Build a view that allows a user to create a new entry or view an existing one. Use a UITextField to capture the title, a UITextView to capture the body, a UIBarButtonItem to save the new 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 add an optionalentry
property to the class. You can remove the navigation boiler-plate code. - 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 (don't forget to constrain it), add an outlet to the class file called
titleTextField
. - Select your UITextField and give it a default placeholder of "Enter title here..."
- Add a UITextView for the entry's body text beneath the title text field and add an outlet to the class file
bodyTextView
. - Give the UITextView a default text of "Write entry here... "
- 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.
Now, we need to add a save button to the top right corner, but our navigation bar is not present because we have not created a segue from the EntryListTableViewController
yet. So let's do that, and then we can wrap up our detail view.
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.
- 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 not only to display an existing entry but also to edit an entry( more on this in day 2).
- Add a
prepare(for segue: UIStoryboardSegue, sender: Any?)
function to the EntryListTableViewController (hint: this comes as part of the boiler-plate code, all you should have to do is uncomment it). - Implement the
prepare(for segue: UIStoryboardSegue, sender: Any?)
function. If the identifier is 'showEntry' (or whatever name you used on Step 2) we will pass the selected entry to the DetailViewController, which will call ourupdateViews()
function (which we will create shortly).
- You will need to capture the selected entry by using the indexPath of the selected row.
- Remember that the
updateViews()
function will update the destination view controller with the entry details.
Hop back to your EntryDetailViewController
and finish out the remaining steps.
- Add a UIBarButtonItem to the UINavigationBar as a
Save
System Item and add an IBAction to the class file calledsaveButtonTapped
. - In the
saveButtonTapped
IBAction, using an if let (conditional unwrapping), check if the optionalentry
property holds an entry. If it does, add a print statement that says "to be implemented tomorrow". If not (meaning if theentry
property is nil), call thecreateEntryWith()
function that lives on theEntryController
. This will require you to use your outlets to access the values in the titleTextField and bodyTextView. - Still inside the
saveButtonTapped
IBAction, but outside the scope of the if let, add code to dismiss the current view and pop back to theEntryListTableViewController
. - Add an
updateViews()
function that checks if the optionalentry
property holds an entry (hint: use a guard statement to do this). 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()
At this point your app should be working almost perfectly. You might notice, however, when you create a new entry and navigate back to the EntryListTableViewController
, your new entry is not there. To fix this, call the method to reload your tableView's data inside of the viewWillAppear()
function on your TableViewController (hint: you will need to add this lifecycle method, viewWillAppear, to your code).
At this point, everything should be working. However, we do still have one final problem. If we stop the app and re-run it, none of our data is there. To solve this, we are going to need to add persistence.
Our EntryController
object is the source of truth for entries. We are now going to add a layer of persistent storage. We need to update the EntryController
to save the entries array to persistent storage when a change happens (whether it be an entry is created or deleted). We will also need to create the functionality to save to the persistent store and load from it.
- 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 documentsDirectoryURL = urls[0].appendingPathComponent("Journal.json")
return documentsDirectoryURL
}
- Write a method called
saveToPersistentStorage()
that will save the current entries array to a file on disk. - Call
encode(value: Encodable) throws
on an instance of 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 try catch block andcatch
any error that throws. - 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 and needs ALL the parts of a do try catch block. - 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, Joey's Mac / 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. In the above code, we are saving our information as .json data.
- Write a method called
loadFromPersistentStorage()
that will load the current data from the file on disk where we saved our entries(data). - Create a constant called
data
to hold the data that you will get back by callingData(contentsOf:)
. Now pass in thefileURL()
as an argument (hint: this is a throwing function). - Call
decode(from:)
on an 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.
- Finally, you need to call the
loadFromPersistentStorage()
function. While there are many different places you could do this successfully, for now, go to theviewDidLoad()
lifecycle method in yourEntryListTableViewController
and callEntryController.shared.loadFromPersistentStorage()
.
- Note: The first time you run your app after implementing your persistence functions you will have no saved data, and therefore you will see an error message in your debug console. This is normal. After you have stored data you should not recieve an error message when
loadFromPersistentStorage()
is called.
Run the app; it should now function properly— make sure to thoroughly test for bugs. You should be able to:
- Upon launch, see a tableView with all entries (there will be no entries if you haven't created any yet, or have deleted all of them).
- Be able to click the add button in the top right corner and navigate to the
EntryDetailViewController
. - Be able to hit the clear button at the bottom of the screen and see you text field and text view become empty.
- Click the save button in the top right corner of the
EntryDetailViewController
and be navigated back to theEntryListTableViewController
, where you should then see your newly created entry. - Be able to click on an entry in the
EntryListTableViewController
and be navigated to theEntryDetailViewController
where you should see that entry's title and body text (if you click save here it should simply navigate you back to theEntryListTableViewController
and your debug console should prompt you with a message that says: "to be implemented tomorrow").
Today you are going to expand upon your Journal application. So far you have a single journal that can store one or many entries. We are going to refactor our application so that we can have an array of journals (ex. Travel Journal, Pain Journal, etc...). In order to do this, a few things are going to have to change. For starters, we won't have a source of truth that contains an array of entries anymore. Instead, you will need to have a source of truth that contains an array of journals, and each journal will hold an array of entries. Here is a breakdown of what you will need to do:
- Create a Journal model.
- Create a Journal model controller.
- Move persistence methods from the EntryController to the JournalController.
- Refactor the storyboards to start with a list of Journals.
- Add an update(entry: Entry) function to the EntryController.
Create a Journal model class that will hold a title and an entries property.
- Add a new
Journal.swift
file and define a newJournal
class. - Add properties for title and entries (hint: entries will be an array of Entry).
- Add a memberwise initializer that takes parameters for each property.
- Consider setting a default parameter value for the entries array.
Create a model object controller called JournalController
that will manage creating, deleting and updating journals. Steps 6 and 7 are going to take a little bit of thinking. Give it your best shot! If you are stuck for more than 20 minutes send a message in the queue channel.
- Add a new
JournalController.swift
file and define a newJournalController
class within then class. - Create a
shared
property as a shared instance. - Add a
journals
array property, and set its value to an empty array ofJournal
. - Create a
createJournalWith(title: String)
function that takes in atitle
. It should create a new instance ofJournal
and add it to thejournals
array - Create a
delete(journal: Journal)
function that removes the journal from thejournals
array.
- 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
Journal
class a way to find equal objects. To resolve the error, implement the Equatable protocol on theJournal
class.
- Create an
addEntryTo(journal: ...)
function that should take in an existing journal as a parameter as well as an entry. In the body of this function append the entry to the journals array of entries. - Create a
removeEntryFrom(journal: ...)
function that should take in an existing journal as a parameter as well as an entry. In the body of this function you will need to find the index of the given entry, and then remove the object at that index from the given journal's array of entries.
With our EntryController
no longer being the best location for our source of truth, we are going to have to refactor it quite significantly. Starting with changing the location of our persistence functions.
- Copy the
fileURL()
function on ourEntryController
and paste it at the bottom of theJournalController
. - Write a
saveToPersistentStorage()
method that will save the current journals array (hint: this will look exactly like thesaveToPersistentStorage()
method you have on theEntryController
, except it will save journals instead of entries).
- You code will likely give you an error. We cannot encode or decode without first making our model Codable.
- Write a
loadFromPersistentStorage()
method that will load the saved data (hint: this will look exactly like theloadFromPersistentStorage()
method you have on theEntryController
, except it will load journals instead of entries). - You can now delete the persistence functions (
fileURL()
,saveToPersistentStorage()
, andloadFromPersistentStorage()
) from theEntryController
altogether. You will also need to go into each CRUD function on theEntryController
and remove thesaveToPersistentStorage()
function. - Back on the
JournalController
, make sure to callsaveToPersistentStorage()
at the end of each one of your CRUD functions, if you have not already.
We still need to do a little more refactoring to our EntryController
.
- We no longer need an entries array (former source of truth), because our entries will now be on a journal object. So delete
var entries: [Entry] = []
(ignore any errors for the moment). - Beause we no longer have an entries source of truth, there is not a need to have a shared instance. Sure, it gives us access to these functions, but that is not a good enough reason to have a shared instance. So delete
static let shared = EntryController()
.
We have now trimmed down our EntryController
significantly, and what remains has errors. The remaining code is attempting to utilize the entries array that no longer exists.
- In the
createEntryWith()
function, removeentries.append(newEntry)
and instead call theaddEntryTo()
function that lives on yourJournalController
. You will note that this function requires you to pass in a journal, which means you will also need to modify thecreateEntryWith()
function parameters to take in a journal and then pass that journal into yourcreateEntryWith()
function. - In the
deleteEntry()
function, remove all the code. None of it will work. However, you have already written the code needed for this work. Simply call yourremoveEntryFrom()
function located in yourJournalController
. Like Step 3, this function requires a journal. So, modify the thedeleteEntry()
function parameters to take in a journal and then pass that journal into yourremoveEntryFrom()
function. - Now, because
EntryController
no longer has a shared instance, these functions are no longer accessible. To make them accessible, add thestatic
keyword before both functions.
If you were to build now you would notice we have a few errors. That is because you have multiple lines of code referring to an EntryController.shared which no longer exists. Don't worry, it will all be taken care of.
- Delete the Navigation Controller in your Main.storyboard. This is going to make your add button () and save button () dissappear. Don't freak out though. They are actually still hiding in there.
- Add a View Controller (yes - a viewController, not a tableViewController) to your Main.storyboard and then embed it in a Navigation Controller. *Hint: You will need to reset your storyboards inital view controller.
- Add a view to the top of your view controller and constrain it to have a height of 100, be 32 points from the top, and 0 points from the view controllers leading and trailing edges.
- Add a table view to the view controller you just added. Give the table view 1 prototype cell. Set the cell to have a style of right detail. Give the cell an identifier of
journalCell
. - Constrain your table view with 0's on all sides (top should be referring to the view you just added).
- Add a show segue from the cell to your
EntryListTableViewController
. This should bring your add bar button and save bar button back. Give the segue an identifier oftoEntryList
. - Add a textField and a button to the view at the top of your view controller. Embed them in a stack view and make sure the axis is set to vertical. Set the alignment to Fill, the distribution to Fill Equally, and the spacing to 8.
- Constrain the stack view to be centered horizontally and vertically in the view. Set it to have a width equal to 80% of the view.
- Give the textField some placeholder text like,
Enter Journal Title Here...
and give the button a label like,Create New Journal
.
You might have noticed that the view controller still has no class, and that is correct. That is because we have not created a view controller file to control this view controller scene. Next you will create that view controller file, connect the outlets and actions, and build out the necessary functions. After that, you will modify the EntryListTableViewController
and EntryDetailViewController
as needed.
Create a view controller called JournalListViewController
that will manage your Journal List view controller scene.
- Add a new
JournalListViewController
file (subclass of a UIViewController) and delete boiler-plate comments that you don't want. - Class your Journal List view controller in your Main.storyboard as a
JournalListViewController
. - Connect your text field outlet and name it
journalTitleTextField
. - Connect your create new journal button action and name it
createNewJournalButtonTapped
. You will come back and build out this function on step 8. - Connect your tableView outlet and name it
journalListTableView
. *Don't forget to set your dataSource and delegate for yourjournalListTableView
(hint: this will prompt you with an error, fix it). - Build out your two dataSource functions
numberOfRowsInSection
andcellForRowAt
. *IncellForRowAt
, include the title of your journal and the count of it's entries. You will notice thatcellForRowAt
does not include a default cell that a tableViewController provides for you. You will need to add this yourself. Give it your best shot. Reference your EntryListTableViewController for help. Reach out in the queue channel if you have not solved it after 20 minutes. - Now, when you navigate (or segue) over to your
EntryListTableViewController
, you need to tell yourEntryListTableViewController
which journal's entries to load. This is whereprepare(for segue)
is crucial. This function allows you to pass a specific journal over. Build out yourprepare(for segue)
function, and pass over whichever journal the user selected. *Hint 1: You will notice that you don't have access to a tableView in yourprepare(for segue)
function. You will need to access theindexPathForSelectedRow
of yourjournalListTableView
. *Hint 2: Make sure to go to yourEntryListTableViewController
and give it a landing pad to receive a journal. This should be an optional Journal. - Let's not forget about our
createNewJournalButtonTapped
. In the body of this IBAction, use the text from yourjournalTitleTextField
(making sure it is not empty) to create a new journal. After calling yourcreateJournalWith()
function, tell yourjournalListTableView
to reload its data, and set thejournalTitleTextField
back to an empty string. - Finally, before you move on, go up to your lifecycle methods and the
viewWillAppear()
method. Inside this method, tell your table view to reload its data. That way, when you navigate back to this page, you can show the updated amount of entries in any given journal.
Finally, you can fix some of those errors that have been showing on your EntryListTableViewController
. To do this, you are going to refactor your dataSource methods to refer to the entries of the Journal you just passed over, instead of using EntryController.shared.entries
, which no longer exists. (Hint: Make sure you are using auto-complete as you type your code.)
- Remove the
EntryController.shared.loadFromPersistentStorage()
line of code in yourviewDidLoad()
. It is no longer necessary. Our app now loads our data when it launches to theJournalListViewController
. - Update your
numberOfRowsInSection
method to return the count of the entries of your journal property. *Hint: Because the journal is optional, and cannot be guaranteeed to be there, you will need unwrap it. Or, this might be a good ocassion to use nil-coalescing. - Update your
cellForRowAt
method to refer to a specific entry from the array of entries on your journal property. *Hint: Because the journal is optional, and cannot be guaranteeed to be there, you will need unwrap it. If it is nil, you will want to return aUITableViewCell()
. - Update your
commit editingStyle
method to refer to a specific entry from the array of entries on your journal property. Delete theEntryController.shared.deleteEntry(entry: entryToDelete)
line of code and retype it. This time, you will not need to use the a shared instance. So your line of code should look something like this:EntryController.deleteEntry(entry: <#T##Entry#>, journal: <#T##Journal#>)
. Make sure to pass in your entry and journal. *Hint: Because the journal is optional, and cannot be guaranteeed to be there, you will need unwrap it. *Step 4 is a tough one. Give it your best attempt. If you cannot solve it after 20 minutes, send a message in the queue channel. - Like with steps 2, 3, and 4, you will need to update your
prepare(for segue)
method to refer to a specific entry from your journal property. *Hint: Because the journal is optional, and cannot be guaranteeed to be there, you will need unwrap it.
You will notice you still have one error on your EntryDetailViewController
. It is trying to call EntryController.shared
which no longer exists.
- Delete that line of code and recall the function without using a shared instance. *You will see that there is still a problem. You need to pass in a journal, but do not have access to one. You will fix this over the next few steps.
- Add an optional journal property below your
var entry: Entry?
. - Within your guard statement in
saveButtonTapped()
, unwrap your optional journal. - Pass in the unwrapped title, body, and journal to your
EntryController.createEntryWith()
function.
- Go back to your
prepare(for segue)
method on yourEntryListTableViewController
and pass over your unwrapped journal to your destination's journal property. - Now, while this will pass a journal over to the
EntryDetailViewController
if you select an existing journal, it will not work if you click the add button in the top right hand corner to create a new entry. To fix this, you need to look at yourprepare(for segue)
method. It has code that checksif segue.identifier == "showEntry"
. If it does not, however, we still need to pass the journal over. So, add onelse if segue.identifier == "createNewEntry"
. In here, you will need to unwrap the destination and journal, and set the destination's journal property to the value of that unwrapped journal. *Hint: Don't forget to give the segue an identifier on your Main.storyboard.
Run your app. You should see a near-complete, working app. If you do not, spend 20 minutes debugging and then send a message in the queue channel if your bugs are not resolved. You have one final change to make. At the moment, if we click on a journal's entry, we can see it displayed in the detail view. However, if we make changes to it and click save, none of the changes are actually saved. Instead we see a print out in our debug console saying "We will handle this tomorrow." Well, tomorrow is today, so let's handle it...
- At the moment, we don't have a function to update an entry, so go to your
EntryController
and add a static function calledupdate()
. It should take in 3 parameters: an entry, a title, and a body. - In the body of
update()
, set the passed in entry's title to the passed in title, and the entry's body to the passed in body. *Hint: If you get any errors here, go and check the properties on yourEntry
model. Are they constants or variables? - Now, you need to save your changes. In order to do this, call the
saveToPersistentStorage()
function on yourJournalController
.
- Go to your
EntryDetailViewController
and, inside yoursaveButtonTapped()
IBAction, replaceprint("We will handle this tomorrow.")
with a call to your newly createdupdate()
function on yourEntryController
.
Run your app. It should be working perfectly now! If it does not, spend 20 minutes debugging, and if you cannot solve the problem please send a message in the queue channel.
You might be thinking, "This was a lot of work to refactor this application. Might it have been easier just to rebuild it from scratch?" It might have been. This prompts two thoughts. Firstly, this demonstrates the importance of planning and understanding what you want to include in an app. Proper planning will often save you a lot of time coding. Secondly, it is still of immense benefit to have to refactor applications while learning. It helps you develop a better understanding of how data is moving around your app. Use refactoring opportunities for this purpose. It is also worth keeping in mind, if you are working on an app that is thousands of lines long you will most certainly not want to rebuild the whole application.
- You might have noticed that your Entry model includes a timestamp, but it has not been used anywhere. Update your
EntryListTableViewController
scene to have a style of right detail instead of basic. Update yourEntryListTableViewController
file to show the timestamp in the detail text view. Hint: Do some research onDateFormatter()
, it can provide you some ways of turning a swift date into a nice looking string. - On your JournalListViewController, adjust the
Create New Journal
button to be grayed out and unselectable if thejournalTitleTextField
is empty. - Add support for tags on journals, add functionality to select a tag to display a list of entries with that tag.
- Set the delegate relationship by adopting the UITextFieldDelegate protocol in the
EntryDetailViewController
class. Implement the delegate functiontextFieldShouldReturn
and call the resignFirstResponder() method on the titleTextField to dismiss the keyboard.
© 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.