From b2712678460f31fcf1ff1e08791335ac24f6ebbc Mon Sep 17 00:00:00 2001 From: Frank Martin Date: Mon, 25 Feb 2019 09:24:25 -0700 Subject: [PATCH 1/3] adds more of ReadMe --- .../Day 3/Task 1 - Part 1.md | 162 ++++++++++++++++++ .../Day 3/Task/Task.xcodeproj/project.pbxproj | 10 +- .../complete.imageset/Contents.json | 21 +++ .../complete.imageset/complete.png | Bin 0 -> 13278 bytes .../incomplete.imageset/Contents.json | 21 +++ .../incomplete.imageset/incomplete.png | Bin 0 -> 7295 bytes .../Task/Task/Base.lproj/Main.storyboard | 6 +- .../Day 3/Task/Task/CoreDataStack.swift | 2 +- .../Day 3/Task/Task/SwitchTableViewCell.swift | 15 ++ .../Day 3/Task/Task/Task + Convenience.swift | 2 +- .../Task.xcdatamodel/contents | 8 +- .../Day 3/Task/Task/TaskController.swift | 64 +++++++ .../Task/TaskDetailTableViewController.swift | 70 +------- .../Task/TaskListTableViewController.swift | 55 +----- 14 files changed, 317 insertions(+), 119 deletions(-) create mode 100644 Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/complete.imageset/Contents.json create mode 100644 Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/complete.imageset/complete.png create mode 100644 Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/incomplete.imageset/Contents.json create mode 100644 Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/incomplete.imageset/incomplete.png create mode 100644 Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/SwitchTableViewCell.swift create mode 100644 Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/TaskController.swift diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task 1 - Part 1.md b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task 1 - Part 1.md index 04c5998..fe19993 100644 --- a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task 1 - Part 1.md +++ b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task 1 - Part 1.md @@ -26,6 +26,9 @@ Students who complete this project independently are able to: * use an NSFetchedResultsController to populate a UITableView with information from Core Data * implement the NSFetchedResultsControllerDelegate to observe changes in Core Data information and update the display accordingly +### Set up +Add the complete and incomplete images to your Assets folder. Images can be found ~~~~~~~~~~~~~~here~~~~~~~~~~~~~ + # Part One 1. Add a `UITableViewController` scene to your storyboard. This table view will be used to list tasks. @@ -81,3 +84,162 @@ Now you need to add a convenience initializer for your `Task` objects that match * note: Make sure the initializer has parameters for `name`, `notes`, `due`, and `context` and that each parameter takes in the right type (`context` will be of type `NSManagedObjectContext`). * note: Remember that `notes` and `due` are optional, therefore, you can give them default values of `nil`. Also, give `context` a default value of `CoreDataStack.context`. 3. Inside the body of the initializer, call the `NSManagedObject` convenience initializer and pass in `context` from your own convenience initializer --> `self.init(context: context)`. Next, set your `Task` properties (self.name = name... etc.) + +Note: Make sure that you import `CoreData`. + +``` +extension Task { + + convenience init(name: String, notes: String? = nil, due: Date? = nil, context: NSManagedObjectContext = CoreDataStack.managedObjectContext) { + self.init(context: context) + self.name = name + self.notes = notes + self.due = due + } +} +``` +This is the initializer you will use moving forward to create a new instance of a `Task`. + +Now that we have implemented the Core Data model, let's build out our model controller. + +### Create the Task model controller + +Let's create our CRUD functions including saving and loading from Core Data. + +Create a new Swift file and name it `TaskController`. This controller will have the sole responsibility of making creating and updating the `Task` model objects in our app. First things first, declare a new class named `TaskController` and then add a shared instance. Finally, add our source of truth, which will be an empty array of `Task` objects. Your code should look something like the following: + +``` +class TaskController { + + // Shared instance + static let shared = TaskController() + + // Soure of truth + var tasks: [Task] = [] +} +``` + +##### Create +Let's start off by creating our create function. First, import `CoreData` at the top of the file. Now, create a function called `createTaskWith` that takes in the following parameters: + +1. A `name` of type `String` +2. An optional `notes` of type `String` +3. An optiopnal `due` of type `Date` + +Next, in the body of our function, initialize a new `Task` object by calling the convenience initializer we created earlier. Be sure to wildcard the name of your new instance since we aren't actually going directly use the object. + +``` + func createTaskWith(name: String, notes: String?, due: Date?) { + + // Initialize a new Task object + let _ = Task(name: name, notes: notes, due: due) + + // TODO: - Save the managed object context + } + +``` + +##### Update +Add an `update` function that takes in the following parameters: + +1. The `Task` object to be updated +2. A `name` of type `String` +3. An optional `notes` of type `String` +4. An optiopnal `due` of type `Date` + +In the body of our function, set the `task's` name, notes, and due to the parameters that were passed into the function. + +``` + func update(_ task: Task, _ name: String, notes: String?, due: Date?) { + + // Set the tasks's properties to the parameters that were passed in + task.name = name + task.notes = notes + task.due = due + + // TODO: - Save the managed object context + } +``` + +##### Delete +Next, we are going to create our delete function. add a `delete` function that takes in a `Task` object. This function will be very simple. In the body of the function, call `delete` and on the `CoreDataStack's` `context`. Pass the `task` into the function. + +``` + func delete(_ task: Task) { + + // Remove the task from the Managed Object Context + CoreDataStack.managedObjectContext.delete(task) + + // TODO: - Save the managed object context + } + +``` + +Now that we are done with creating, updating, and deleting model objects, we need to add functionality so that our app acutally saves changes to the `Persistent Store`. + +##### Save to the Persistent Store + +Think of the Persistent Store as the user's iPhone hard drive. We've made changes to the Managed Object Context, but those changes have not been saved to the persisent store, yet; hence, the `TODO's` that are in our code. + +Create a new function called `saveToPersistentStore` that takes in no parameters. In the body of the function. Add a `do-catch` block. Inside of the block, call `save` on your `CoreDataStack.context`. Make sure that you mark your call with `try` and catch/handle any possible errors that are thrown. + +``` + func saveToPersistentStore() { + do { + try CoreDataStack.context.save() + } catch { + print("There was an error saving to the persistent store. \(error)") + } + } +``` + +Now that we are able to save to the Persistent Store, let's call our `save` function in `createTaskWith()`, `update`, and `delete`. Note that making changes to the managed object context does not actually mean they have been saved to the Persistent Store. Once we call the save functions, the current state of the managed object context is saved. + +##### Load from persistent store + +We are going to revisit the `TaskController's` source of truth. Instead of having `tasks` equal an empty array, let's turn it into a computed property. Inside the body of the property's definition, create a new constant of type `NSFetchRequest`. Make sure you specify the instance's generic as `Task`. Have your new constant equal a new instance of `NSFetchRequest` and pass in "Task" as the `entityName`. Next, in a `do-catch` block, return the results of calling `fetch` on your `CoreDataStack.context` and pass in the `fetchRequest`. This method returns all of the managed objects that you request in your fetch request in an array. Catch any errors and return an empty array in the `catch`. + +``` + // Source of truth + var tasks: [Task] { + + // Create the fetch request + let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "Task") + do { + // Return the results of the fetch request. + return try CoreDataStack.context.fetch(fetchRequest) + } catch { + print("There was an error fetching the Tasks: \(error)") + return [] + } + } + +``` + +### Wire up storyboard to code + +Let's shift our focus to the UI of the app. Create a new subclass file of UITableViewCell named `SwitchTableViewCell`. Add one variable in the class named `task` of type `Task`. This will be used as our landing pad. We will revisit the cell later. + +##### Task List Table View Controller + +First, add the table view's data source. In `numberOfRows`, return the count of the `TaskController's` `tasks` array. + +In `cellForRowAt`, populate each cell with the appropriate task. First, let's wrap the cell initialization in a `guard` statement, and optionally downcast the cell as a `SwitchTableViewCell`. In your `else` statement, return an instance of `UITableViewCell`. Give the cell a reuse identifier both in code and in your storyboard. +Now, index the source of truth using the `indexPath` passed into the function. Once you have the task for that particular cell, pass it to the cell's landing pad. + +``` +guard let cell = tableView.dequeueReusableCell(withIdentifier: "taskCell", for: indexPath) as? SwitchTableViewCell else { return UITableViewCell() } + +let task = TaskController.shared.tasks[indexPath.row] + cell.task = task + +return cell +``` + +Go into your `Main.storyboard` file and give an identifier to the segue from your cell to the TaskDetailViewController; it should be something to the effect of "toTaskDetail." + +Let's handle when a user taps on an existing task. In your `TaskListTableViewController.swift` file, uncomment the `prepareForSegue` function. Check that the segue's identifier matches the one that you just set in storyboard; if it matches, unwrap the segue's destination view controller and the table view's selected index path. Index the `tasks` array on your `TaskController` and pass the correct `Task` object to your detail view controller. + + + + diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task.xcodeproj/project.pbxproj b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task.xcodeproj/project.pbxproj index 6f9febf..3d8e06f 100644 --- a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task.xcodeproj/project.pbxproj +++ b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + B73804E7221DCC8E00CD9E59 /* TaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B73804E6221DCC8D00CD9E59 /* TaskController.swift */; }; + B73804E9221F729500CD9E59 /* SwitchTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B73804E8221F729500CD9E59 /* SwitchTableViewCell.swift */; }; B761071E22136DDD001F79B6 /* Task + Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = B761071D22136DDD001F79B6 /* Task + Convenience.swift */; }; B7BA6EE221F7A442004A3C70 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7BA6EE121F7A442004A3C70 /* AppDelegate.swift */; }; B7BA6EE721F7A442004A3C70 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B7BA6EE521F7A442004A3C70 /* Main.storyboard */; }; @@ -19,6 +21,8 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + B73804E6221DCC8D00CD9E59 /* TaskController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskController.swift; sourceTree = ""; }; + B73804E8221F729500CD9E59 /* SwitchTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchTableViewCell.swift; sourceTree = ""; }; B761071D22136DDD001F79B6 /* Task + Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Task + Convenience.swift"; sourceTree = ""; }; B7BA6EDE21F7A442004A3C70 /* Task.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Task.app; sourceTree = BUILT_PRODUCTS_DIR; }; B7BA6EE121F7A442004A3C70 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -63,10 +67,12 @@ isa = PBXGroup; children = ( B7BA6EE121F7A442004A3C70 /* AppDelegate.swift */, - B7BA6EF621F7D4D6004A3C70 /* TaskListTableViewController.swift */, B7BA6EFA21F7D585004A3C70 /* CoreDataStack.swift */, B761071D22136DDD001F79B6 /* Task + Convenience.swift */, + B7BA6EF621F7D4D6004A3C70 /* TaskListTableViewController.swift */, + B73804E8221F729500CD9E59 /* SwitchTableViewCell.swift */, B7BA6EF821F7D502004A3C70 /* TaskDetailTableViewController.swift */, + B73804E6221DCC8D00CD9E59 /* TaskController.swift */, B7BA6EE521F7A442004A3C70 /* Main.storyboard */, B7BA6EEB21F7A446004A3C70 /* Assets.xcassets */, B7BA6EED21F7A446004A3C70 /* LaunchScreen.storyboard */, @@ -147,6 +153,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B73804E7221DCC8E00CD9E59 /* TaskController.swift in Sources */, + B73804E9221F729500CD9E59 /* SwitchTableViewCell.swift in Sources */, B7BA6EFB21F7D585004A3C70 /* CoreDataStack.swift in Sources */, B7BA6EF721F7D4D6004A3C70 /* TaskListTableViewController.swift in Sources */, B761071E22136DDD001F79B6 /* Task + Convenience.swift in Sources */, diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/complete.imageset/Contents.json b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/complete.imageset/Contents.json new file mode 100644 index 0000000..f423a4f --- /dev/null +++ b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/complete.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "complete.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/complete.imageset/complete.png b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/complete.imageset/complete.png new file mode 100644 index 0000000000000000000000000000000000000000..92d325e24a8ce1308535df0f96545b516d3b0081 GIT binary patch literal 13278 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 literal 0 HcmV?d00001 diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/incomplete.imageset/Contents.json b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/incomplete.imageset/Contents.json new file mode 100644 index 0000000..917f14a --- /dev/null +++ b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/incomplete.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "incomplete.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/incomplete.imageset/incomplete.png b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/incomplete.imageset/incomplete.png new file mode 100644 index 0000000000000000000000000000000000000000..50ca26847ed01a5e5667978633f2390d99842a9a GIT binary patch literal 7295 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 Y+o}2S)xk7)SqKP+oPQ`e;QRA`0#vg>GXMYp literal 0 HcmV?d00001 diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Base.lproj/Main.storyboard b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Base.lproj/Main.storyboard index 0d498d1..8129173 100644 --- a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Base.lproj/Main.storyboard +++ b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Base.lproj/Main.storyboard @@ -1,5 +1,5 @@ - + @@ -18,7 +18,7 @@ - + @@ -26,7 +26,7 @@ - + diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/CoreDataStack.swift b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/CoreDataStack.swift index 026550c..3dbeedf 100644 --- a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/CoreDataStack.swift +++ b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/CoreDataStack.swift @@ -23,5 +23,5 @@ struct CoreDataStack { return container }() - static var managedObjectContext: NSManagedObjectContext { return container.viewContext } + static var context: NSManagedObjectContext { return container.viewContext } } diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/SwitchTableViewCell.swift b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/SwitchTableViewCell.swift new file mode 100644 index 0000000..d837eef --- /dev/null +++ b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/SwitchTableViewCell.swift @@ -0,0 +1,15 @@ +// +// SwitchTableViewCell.swift +// Task +// +// Created by Frank Martin on 2/21/19. +// Copyright © 2019 DevMtnStudent. All rights reserved. +// + +import UIKit + +class SwitchTableViewCell: UITableViewCell { + + var task: Task? + +} diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Task + Convenience.swift b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Task + Convenience.swift index 8062c2d..2ef3810 100644 --- a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Task + Convenience.swift +++ b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Task + Convenience.swift @@ -11,7 +11,7 @@ import CoreData extension Task { - convenience init?(name: String, notes: String? = nil, due: Date? = nil, context: NSManagedObjectContext = CoreDataStack.managedObjectContext) { + convenience init(name: String, notes: String? = nil, due: Date? = nil, context: NSManagedObjectContext = CoreDataStack.context) { self.init(context: context) self.name = name self.notes = notes diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Task.xcdatamodeld/Task.xcdatamodel/contents b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Task.xcdatamodeld/Task.xcdatamodel/contents index 506393a..6d4cb19 100644 --- a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Task.xcdatamodeld/Task.xcdatamodel/contents +++ b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Task.xcdatamodeld/Task.xcdatamodel/contents @@ -1,10 +1,10 @@ - + - + - - + + diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/TaskController.swift b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/TaskController.swift new file mode 100644 index 0000000..cc60301 --- /dev/null +++ b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/TaskController.swift @@ -0,0 +1,64 @@ +// +// TaskController.swift +// Task +// +// Created by Frank Martin on 2/20/19. +// Copyright © 2019 DevMtnStudent. All rights reserved. +// + +import Foundation +import CoreData + +class TaskController { + + // Shared instance + static let shared = TaskController() + + // Source of truth + var tasks: [Task] { + + // Create the fetch request + let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "Task") + do { + // Return the results of the fetch request. + return try CoreDataStack.context.fetch(fetchRequest) + } catch { + print("There was an error fetching the Tasks: \(error)") + return [] + } + } + + func createTaskWith(_ name: String, _ notes: String?, _ due: Date?) { + + // Initialize a new Task object + let _ = Task(name: name, notes: notes, due: due) + saveToPersistentStore() + } + + func update(_ task: Task, _ name: String, notes: String?, due: Date?) { + + // Set the tasks's properties to the parameters that were passed in + task.name = name + task.notes = notes + task.due = due + + saveToPersistentStore() + + } + + func delete(_ task: Task) { + + // Remove the task from the Managed Object Context + CoreDataStack.context.delete(task) + saveToPersistentStore() + } + + func saveToPersistentStore() { + do { + try CoreDataStack.context.save() + } catch { + print("There was an error saving to the persistent store. \(error)") + } + } + +} diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/TaskDetailTableViewController.swift b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/TaskDetailTableViewController.swift index a1adc60..452b2d8 100644 --- a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/TaskDetailTableViewController.swift +++ b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/TaskDetailTableViewController.swift @@ -9,77 +9,23 @@ import UIKit class TaskDetailTableViewController: UITableViewController { - + override func viewDidLoad() { super.viewDidLoad() - - // Uncomment the following line to preserve selection between presentations - // self.clearsSelectionOnViewWillAppear = false - - // Uncomment the following line to display an Edit button in the navigation bar for this view controller. - // self.navigationItem.rightBarButtonItem = self.editButtonItem + } - + // MARK: - Table view data source - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - // #warning Incomplete implementation, return the number of rows - return 0 - } - - /* - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath) - - // Configure the cell... - - return cell - } - */ - - /* - // Override to support conditional editing of the table view - override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { - // Return false if you do not want the specified item to be editable. - return true - } - */ - - /* + // Override to support editing the table view. - override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { + override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { - // Delete the row from the data source tableView.deleteRows(at: [indexPath], with: .fade) - } else if editingStyle == .insert { - // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view - } - } - */ - - /* - // Override to support rearranging the table view. - override func tableView(_ tableView: UITableView, moveRowAt fromIndexPath: IndexPath, to: IndexPath) { - + } } - */ - - /* - // Override to support conditional rearranging of the table view. - override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { - // Return false if you do not want the item to be re-orderable. - return true - } - */ - - /* + // MARK: - Navigation - - // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - // Get the new view controller using segue.destination. - // Pass the selected object to the new view controller. } - */ - } diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/TaskListTableViewController.swift b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/TaskListTableViewController.swift index 61afab1..31a4683 100644 --- a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/TaskListTableViewController.swift +++ b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/TaskListTableViewController.swift @@ -12,45 +12,23 @@ class TaskListTableViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() - - // Uncomment the following line to preserve selection between presentations - // self.clearsSelectionOnViewWillAppear = false - - // Uncomment the following line to display an Edit button in the navigation bar for this view controller. - // self.navigationItem.rightBarButtonItem = self.editButtonItem + } // MARK: - Table view data source - override func numberOfSections(in tableView: UITableView) -> Int { - // #warning Incomplete implementation, return the number of sections - return 0 - } - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - - return 0 + return TaskController.shared.tasks.count } - - - /* override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath) - - // Configure the cell... + guard let cell = tableView.dequeueReusableCell(withIdentifier: "taskCell", for: indexPath) as? SwitchTableViewCell else { return UITableViewCell() } + let task = TaskController.shared.tasks[indexPath.row] + cell.task = task + return cell } - */ - - /* - // Override to support conditional editing of the table view. - override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { - // Return false if you do not want the specified item to be editable. - return true - } - */ /* // Override to support editing the table view. @@ -64,29 +42,12 @@ class TaskListTableViewController: UITableViewController { } */ - /* - // Override to support rearranging the table view. - override func tableView(_ tableView: UITableView, moveRowAt fromIndexPath: IndexPath, to: IndexPath) { - - } - */ - - /* - // Override to support conditional rearranging of the table view. - override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { - // Return false if you do not want the item to be re-orderable. - return true - } - */ - - /* // MARK: - Navigation // In a storyboard-based application, you will often want to do a little preparation before navigation override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - // Get the new view controller using segue.destination. - // Pass the selected object to the new view controller. + if segue.identifier == "" } - */ + } From 141fc549a8227be1537a5a0cfef4bbb5e51c8e0e Mon Sep 17 00:00:00 2001 From: Frank Martin Date: Wed, 27 Feb 2019 12:17:29 -0700 Subject: [PATCH 2/3] finishes master for Task pt 1, needs review --- .../Day 3/Task 1 - Part 1.md | 240 ++++++++++++++---- .../Day 3/Task/Task.xcodeproj/project.pbxproj | 74 +++++- .../Task/Task/Base.lproj/Main.storyboard | 120 --------- .../Model Controller}/TaskController.swift | 12 +- .../TaskDetailTableViewController.swift | 55 ++++ .../TaskListTableViewController.swift | 42 ++- .../Task/Task/{ => Model}/CoreDataStack.swift | 0 .../Task/{ => Model}/Task + Convenience.swift | 0 .../Task.xcdatamodeld/.xccurrentversion | 0 .../Task.xcdatamodel/contents | 0 .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../complete.imageset/Contents.json | 0 .../complete.imageset/complete.png | Bin .../incomplete.imageset/Contents.json | 0 .../incomplete.imageset/incomplete.png | Bin .../Base.lproj/LaunchScreen.storyboard | 0 .../Task/Resources/Base.lproj/Main.storyboard | 235 +++++++++++++++++ .../Task/Task/{ => Resources}/Info.plist | 0 .../Day 3/Task/Task/SwitchTableViewCell.swift | 15 -- .../Task/TaskDetailTableViewController.swift | 31 --- .../Task/Task/Views/SwitchTableViewCell.swift | 50 ++++ 22 files changed, 636 insertions(+), 238 deletions(-) delete mode 100644 Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Base.lproj/Main.storyboard rename Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/{ => Controllers/Model Controller}/TaskController.swift (84%) create mode 100644 Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Controllers/View Controllers/TaskDetailTableViewController.swift rename Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/{ => Controllers/View Controllers}/TaskListTableViewController.swift (53%) rename Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/{ => Model}/CoreDataStack.swift (100%) rename Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/{ => Model}/Task + Convenience.swift (100%) rename Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/{ => Model}/Task.xcdatamodeld/.xccurrentversion (100%) rename Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/{ => Model}/Task.xcdatamodeld/Task.xcdatamodel/contents (100%) rename Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/{ => Resources}/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/{ => Resources}/Assets.xcassets/Contents.json (100%) rename Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/{ => Resources}/Assets.xcassets/complete.imageset/Contents.json (100%) rename Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/{ => Resources}/Assets.xcassets/complete.imageset/complete.png (100%) rename Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/{ => Resources}/Assets.xcassets/incomplete.imageset/Contents.json (100%) rename Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/{ => Resources}/Assets.xcassets/incomplete.imageset/incomplete.png (100%) rename Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/{ => Resources}/Base.lproj/LaunchScreen.storyboard (100%) create mode 100644 Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Resources/Base.lproj/Main.storyboard rename Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/{ => Resources}/Info.plist (100%) delete mode 100644 Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/SwitchTableViewCell.swift delete mode 100644 Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/TaskDetailTableViewController.swift create mode 100644 Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Views/SwitchTableViewCell.swift diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task 1 - Part 1.md b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task 1 - Part 1.md index fe19993..472f578 100644 --- a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task 1 - Part 1.md +++ b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task 1 - Part 1.md @@ -106,7 +106,7 @@ Now that we have implemented the Core Data model, let's build out our model cont Let's create our CRUD functions including saving and loading from Core Data. -Create a new Swift file and name it `TaskController`. This controller will have the sole responsibility of making creating and updating the `Task` model objects in our app. First things first, declare a new class named `TaskController` and then add a shared instance. Finally, add our source of truth, which will be an empty array of `Task` objects. Your code should look something like the following: +Create a new Swift file and name it `TaskController`. This controller will have the sole responsibility of creating and updating the `Task` model objects in our app. First things first, declare a new class named `TaskController` and then add a shared instance. Finally, add our source of truth, which will be an empty array of `Task` objects. Your code should look something like the following: ``` class TaskController { @@ -129,13 +129,13 @@ Let's start off by creating our create function. First, import `CoreData` at the Next, in the body of our function, initialize a new `Task` object by calling the convenience initializer we created earlier. Be sure to wildcard the name of your new instance since we aren't actually going directly use the object. ``` - func createTaskWith(name: String, notes: String?, due: Date?) { - - // Initialize a new Task object - let _ = Task(name: name, notes: notes, due: due) - - // TODO: - Save the managed object context - } +func createTaskWith(name: String, notes: String?, due: Date?) { + + // Initialize a new Task object + let _ = Task(name: name, notes: notes, due: due) + + // TODO: - Save the managed object context +} ``` @@ -150,28 +150,28 @@ Add an `update` function that takes in the following parameters: In the body of our function, set the `task's` name, notes, and due to the parameters that were passed into the function. ``` - func update(_ task: Task, _ name: String, notes: String?, due: Date?) { - - // Set the tasks's properties to the parameters that were passed in - task.name = name - task.notes = notes - task.due = due - - // TODO: - Save the managed object context - } +func update(_ task: Task, _ name: String, notes: String?, due: Date?) { + + // Set the tasks's properties to the parameters that were passed in + task.name = name + task.notes = notes + task.due = due + + // TODO: - Save the managed object context +} ``` ##### Delete Next, we are going to create our delete function. add a `delete` function that takes in a `Task` object. This function will be very simple. In the body of the function, call `delete` and on the `CoreDataStack's` `context`. Pass the `task` into the function. ``` - func delete(_ task: Task) { - - // Remove the task from the Managed Object Context - CoreDataStack.managedObjectContext.delete(task) - - // TODO: - Save the managed object context - } +func delete(_ task: Task) { + + // Remove the task from the Managed Object Context + CoreDataStack.managedObjectContext.delete(task) + + // TODO: - Save the managed object context +} ``` @@ -184,13 +184,13 @@ Think of the Persistent Store as the user's iPhone hard drive. We've made change Create a new function called `saveToPersistentStore` that takes in no parameters. In the body of the function. Add a `do-catch` block. Inside of the block, call `save` on your `CoreDataStack.context`. Make sure that you mark your call with `try` and catch/handle any possible errors that are thrown. ``` - func saveToPersistentStore() { - do { - try CoreDataStack.context.save() - } catch { - print("There was an error saving to the persistent store. \(error)") - } +func saveToPersistentStore() { + do { + try CoreDataStack.context.save() + } catch { + print("There was an error saving to the persistent store. \(error)") } +} ``` Now that we are able to save to the Persistent Store, let's call our `save` function in `createTaskWith()`, `update`, and `delete`. Note that making changes to the managed object context does not actually mean they have been saved to the Persistent Store. Once we call the save functions, the current state of the managed object context is saved. @@ -200,25 +200,25 @@ Now that we are able to save to the Persistent Store, let's call our `save` func We are going to revisit the `TaskController's` source of truth. Instead of having `tasks` equal an empty array, let's turn it into a computed property. Inside the body of the property's definition, create a new constant of type `NSFetchRequest`. Make sure you specify the instance's generic as `Task`. Have your new constant equal a new instance of `NSFetchRequest` and pass in "Task" as the `entityName`. Next, in a `do-catch` block, return the results of calling `fetch` on your `CoreDataStack.context` and pass in the `fetchRequest`. This method returns all of the managed objects that you request in your fetch request in an array. Catch any errors and return an empty array in the `catch`. ``` - // Source of truth - var tasks: [Task] { - - // Create the fetch request - let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "Task") - do { - // Return the results of the fetch request. - return try CoreDataStack.context.fetch(fetchRequest) - } catch { - print("There was an error fetching the Tasks: \(error)") - return [] - } +// Source of truth +var tasks: [Task] { + + // Create the fetch request + let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "Task") + do { + // Return the results of the fetch request. + return try CoreDataStack.context.fetch(fetchRequest) + } catch { + print("There was an error fetching the Tasks: \(error)") + return [] } +} ``` ### Wire up storyboard to code -Let's shift our focus to the UI of the app. Create a new subclass file of UITableViewCell named `SwitchTableViewCell`. Add one variable in the class named `task` of type `Task`. This will be used as our landing pad. We will revisit the cell later. +Let's shift our focus to the UI of the app. Create a new subclass file of UITableViewCell named `SwitchTableViewCell`. Add one variable in the class named `task` of type `Task`. This will be used as our landing pad. We will revisit this cell later. ##### Task List Table View Controller @@ -236,10 +236,160 @@ let task = TaskController.shared.tasks[indexPath.row] return cell ``` -Go into your `Main.storyboard` file and give an identifier to the segue from your cell to the TaskDetailViewController; it should be something to the effect of "toTaskDetail." +###### Passing data +Let's handle when a user taps on an existing task. First, go into your `Main.storyboard` file and give an identifier to the segue from your cell to the TaskDetailViewController; it should be something to the effect of "toTaskDetail." Next, in your `TaskListTableViewController.swift` file, uncomment the `prepareForSegue` function. Check that the segue's identifier matches the one that you just set in storyboard; if it matches, unwrap the segue's destination view controller and the table view's selected index path. Add an optional `Task` landing pad on your `TaskDetailViewController`. Index the `tasks` array on your `TaskController` and pass the correct `Task` object to your detail view controller. + +``` +override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + + // Check that we are firing the code for the correct segue + if segue.identifier == "toTaskDetail" { + + // Cast the destination view controller as our custom detail vc + guard let destinationVC = segue.destination as? TaskDetailTableViewController, + + // Grab the indexPath of the row that was selected + let indexPath = tableView.indexPathForSelectedRow + else { return } + + // Index the source of truth + let task = TaskController.shared.tasks[indexPath.row] + + // Pass the task to the detail vc + destinationVC.task = task + } +} + +``` + +###### Deleting a task + +Uncomment the `tableView(commit editingStyle:)` function. We will use this to delete an existing task. In the `if` statement where editing style is 'delete,' index the shared truth and find the correct task to delete and call the `TaskController's` `delete` function. Pass the task to the `delete` function. Remember, always modify your data source (in this case it's our `tasks` array) before actually calling `tableView.deleteRows` or you will get a crash. + +``` +override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + if editingStyle == .delete { + + let task = TaskController.shared.tasks[indexPath.row] + + // Delete the row from the data source + TaskController.shared.delete(task) + + tableView.deleteRows(at: [indexPath], with: .fade) + } +} + +``` +##### Task Detail Table View Controller + +###### Storyboard + +Let's jump back into our storyboard and put the finishing touches on the detail view controller before jumping into code. Complete the following: + +1. Click on the table view in the view hierarchy. +2. In the Attributes Inspector, increase the sections to three. Each section should only have one row. Give section one a title of 'Name.', section two 'Due', and section three 'Notes.' Because we are using a static table view controller, we can add UI elements straight to our table view cell. +3. Drag out a `UITextField` for both section one and two, and add a `UITextView` to section three. Constrain each element to be 4 points away from the leading, top, trailing, and bottom edges of the respective cell's content view. +4. Drag a `UIDatePicker` into the view hierarchy under First Responder. We will use this date picker later to set the optional `due` property. +5. Add a navigation item and a Save' `UIBarButtonItem` to the right side of the navigation bar. + +###### TaskDetailTableViewController.swift + +1. Drag outlets from your storyboard's view controller into your swift file. Make sure you include an outlet for the date picker. +2. Drag out an `IBAction` for your `saveBarButtonItem`. +3. In `viewDidLoad`, assign the `dueTextField's` `inputView` to your date picker. +4. In your `saveButtonTapped` function, safely unwrap the `nameTextField's` text and either a) create a new instance of `Task`, or b) update an existing task. Remember that `due` and `notes` are both optional. Also note that the data for `due` should come from your date picker, not the `dueTextField` itself. Finally, pop the view controller off the navigation stack. This should happen regardless of whether an update or creation is occuring. + +``` +// Unwrap the name text field and ensure that the string is not empty + guard let name = nameTextField.text, !name.isEmpty else { return } + + // If we are in the detail view for an existing task, unwrap it + if let task = task { + + // Update the task + TaskController.shared.update(task, name: name, notes: notesTextView.text, due: datePicker.date) + + } else { + + // Create a new task + TaskController.shared.createTaskWith(name, notesTextView.text, datePicker.date) + } + + // Pop the detail vc off the navigation stack + self.navigationController?.popViewController(animated: true) +} +``` + +We have to complete a couple of steps in order to get our `dateTextField` to show the date picker's date. Complete the following: + +1. In storyboard, drag out an action from your date picker named `datePickerChanged`. This action will fire any time that your date picker selects a date. +2. In the body of your action, set the `dueTextField's` text to the `datePicker's` date. Hint: use the `sender` that is passed into your action. If the sender isn't already of type `UIDatePicker`, change it now. + +##### Switch Table View Cell + +We can successfully create and update new tasks; however, we still need to be able to display them in the our table view, and provide the functionality for the tasks to be completed. + +In storyboard, navigate to your `TaskListTableViewController`. Add the following: + +1. A label for the task's name. +2. A label for the due date. +3. A button for our complete/incomplete image. Make sure you erase the "Button" placeholder text. You can assign one of the images as a placeholder, make sure you add it to the button's background. +4. Constraints to all of the elements. Set up the cell how you'd like. + +Assign the table view cell's subclass file in the Identity Inspector. Drag outlets into your `SwitchTableViewCell` subclass file. Drag out an action for the `isComplete` button. + +Now that our setup is complete, add logic to your cell. + +1. On our `task` landing pad, add a `didSet`. +2. Add a function named `updateViews` that will update each UI element for the task passed to the cell. Call `updateViews` in your `didSet`. + +``` +public func updateViews() { + guard let task = task else { return } + nameLabel.text = task.name + dueLabel.text = String(describing: task.due) + let image: UIImage? = task.isComplete ? UIImage(named: "complete") : UIImage(named: "incomplete") + isCompleteButton.setBackgroundImage(image, for: .normal) +} + +``` +###### Cell Delegate +When the user taps the `isComplete` button, our table view cell should not be making any changes to the model; i.e. the `isComplete` property should not be changed by the cell. In order to maintain proper separation of concerns, we need to implement a delegate. -Let's handle when a user taps on an existing task. In your `TaskListTableViewController.swift` file, uncomment the `prepareForSegue` function. Check that the segue's identifier matches the one that you just set in storyboard; if it matches, unwrap the segue's destination view controller and the table view's selected index path. Index the `tasks` array on your `TaskController` and pass the correct `Task` object to your detail view controller. +1. Declare a new delegate protocol outside of the class. Name it `SwitchTableViewCellDelegate`. This protocol will only require one function `isCompleteToggled` that will take in a `SwitchTableViewCell` +2. Add a `weak var delegate` of type `SwitchTableViewCellDelegate?` +3. In the `isCompleteButtonTapped` function, call `isCompleteToggled` on your `delegate`. +Switch back to the `TaskListTableViewController` and adopt/conform to the delegate that you just created. +``` +extension TaskListTableViewController: SwitchTableViewCellDelegate { + + func isCompleteToggled(on cell: SwitchTableViewCell) { + + } +} +``` +Next, in `cellForRowAt` make sure you are setting yourself as the cell's delegate. +In the body of the `isCompleteToggled`, we need to have the `TaskController` toggle the `isComplete` property on our task. + +In the `TaskController` add a function that takes in a `Task` and toggles its `isComplete` property, then have it save to Core Data. + +``` +func toggleIsComplete(for task: Task) { + task.isComplete = !task.isComplete + saveToPersistentStore() +} +``` + +In the `TaskListTableViewController` implementation of `isCompleteToggled`, use the cell that is passed into the function to get a reference to the task to be toggled. You will need to unwrap this task. Once you have the task, call the `toggleIsComplete` function that you just created in the `TaskController` and pass it the task. Finally, call `updateViews` on the cell that was passed into the `isCompleteToggled` function. + +``` +func isCompleteToggled(on cell: SwitchTableViewCell) { + guard let task = cell.task else { return } + TaskController.shared.toggleIsComplete(for: task) + cell.updateViews() + } +``` diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task.xcodeproj/project.pbxproj b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task.xcodeproj/project.pbxproj index 3d8e06f..8c8edde 100644 --- a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task.xcodeproj/project.pbxproj +++ b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task.xcodeproj/project.pbxproj @@ -47,6 +47,61 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + B73804EA22248AB600CD9E59 /* Model */ = { + isa = PBXGroup; + children = ( + B761071D22136DDD001F79B6 /* Task + Convenience.swift */, + B7BA6EE821F7A442004A3C70 /* Task.xcdatamodeld */, + B7BA6EFA21F7D585004A3C70 /* CoreDataStack.swift */, + ); + path = Model; + sourceTree = ""; + }; + B73804EB22248AC200CD9E59 /* Controllers */ = { + isa = PBXGroup; + children = ( + B73804EC22248AC800CD9E59 /* View Controllers */, + B73804ED22248ACF00CD9E59 /* Model Controller */, + ); + path = Controllers; + sourceTree = ""; + }; + B73804EC22248AC800CD9E59 /* View Controllers */ = { + isa = PBXGroup; + children = ( + B7BA6EF621F7D4D6004A3C70 /* TaskListTableViewController.swift */, + B7BA6EF821F7D502004A3C70 /* TaskDetailTableViewController.swift */, + ); + path = "View Controllers"; + sourceTree = ""; + }; + B73804ED22248ACF00CD9E59 /* Model Controller */ = { + isa = PBXGroup; + children = ( + B73804E6221DCC8D00CD9E59 /* TaskController.swift */, + ); + path = "Model Controller"; + sourceTree = ""; + }; + B73804EE22248B2900CD9E59 /* Views */ = { + isa = PBXGroup; + children = ( + B73804E8221F729500CD9E59 /* SwitchTableViewCell.swift */, + ); + path = Views; + sourceTree = ""; + }; + B73804EF22248B4900CD9E59 /* Resources */ = { + isa = PBXGroup; + children = ( + B7BA6EE521F7A442004A3C70 /* Main.storyboard */, + B7BA6EEB21F7A446004A3C70 /* Assets.xcassets */, + B7BA6EED21F7A446004A3C70 /* LaunchScreen.storyboard */, + B7BA6EF021F7A446004A3C70 /* Info.plist */, + ); + path = Resources; + sourceTree = ""; + }; B7BA6ED521F7A442004A3C70 = { isa = PBXGroup; children = ( @@ -67,17 +122,10 @@ isa = PBXGroup; children = ( B7BA6EE121F7A442004A3C70 /* AppDelegate.swift */, - B7BA6EFA21F7D585004A3C70 /* CoreDataStack.swift */, - B761071D22136DDD001F79B6 /* Task + Convenience.swift */, - B7BA6EF621F7D4D6004A3C70 /* TaskListTableViewController.swift */, - B73804E8221F729500CD9E59 /* SwitchTableViewCell.swift */, - B7BA6EF821F7D502004A3C70 /* TaskDetailTableViewController.swift */, - B73804E6221DCC8D00CD9E59 /* TaskController.swift */, - B7BA6EE521F7A442004A3C70 /* Main.storyboard */, - B7BA6EEB21F7A446004A3C70 /* Assets.xcassets */, - B7BA6EED21F7A446004A3C70 /* LaunchScreen.storyboard */, - B7BA6EF021F7A446004A3C70 /* Info.plist */, - B7BA6EE821F7A442004A3C70 /* Task.xcdatamodeld */, + B73804EA22248AB600CD9E59 /* Model */, + B73804EB22248AC200CD9E59 /* Controllers */, + B73804EE22248B2900CD9E59 /* Views */, + B73804EF22248B4900CD9E59 /* Resources */, ); path = Task; sourceTree = ""; @@ -308,7 +356,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = W53Z73CT67; - INFOPLIST_FILE = Task/Info.plist; + INFOPLIST_FILE = "$(SRCROOT)/Task/Resources/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -326,7 +374,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = W53Z73CT67; - INFOPLIST_FILE = Task/Info.plist; + INFOPLIST_FILE = "$(SRCROOT)/Task/Resources/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Base.lproj/Main.storyboard b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Base.lproj/Main.storyboard deleted file mode 100644 index 8129173..0000000 --- a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Base.lproj/Main.storyboard +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/TaskController.swift b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Controllers/Model Controller/TaskController.swift similarity index 84% rename from Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/TaskController.swift rename to Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Controllers/Model Controller/TaskController.swift index cc60301..249bd69 100644 --- a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/TaskController.swift +++ b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Controllers/Model Controller/TaskController.swift @@ -21,7 +21,7 @@ class TaskController { let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "Task") do { // Return the results of the fetch request. - return try CoreDataStack.context.fetch(fetchRequest) + return try CoreDataStack.context.fetch(fetchRequest) } catch { print("There was an error fetching the Tasks: \(error)") return [] @@ -35,7 +35,7 @@ class TaskController { saveToPersistentStore() } - func update(_ task: Task, _ name: String, notes: String?, due: Date?) { + func update(_ task: Task, name: String, notes: String?, due: Date?) { // Set the tasks's properties to the parameters that were passed in task.name = name @@ -53,12 +53,16 @@ class TaskController { saveToPersistentStore() } + func toggleIsComplete(for task: Task) { + task.isComplete = !task.isComplete + saveToPersistentStore() + } + func saveToPersistentStore() { do { try CoreDataStack.context.save() } catch { print("There was an error saving to the persistent store. \(error)") } - } - + } } diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Controllers/View Controllers/TaskDetailTableViewController.swift b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Controllers/View Controllers/TaskDetailTableViewController.swift new file mode 100644 index 0000000..4ecf02f --- /dev/null +++ b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Controllers/View Controllers/TaskDetailTableViewController.swift @@ -0,0 +1,55 @@ +// +// TaskDetailTableViewController.swift +// Task +// +// Created by Frank Martin Jr on 1/22/19. +// Copyright © 2019 DevMtnStudent. All rights reserved. +// + +import UIKit + +class TaskDetailTableViewController: UITableViewController { + + // MARK: - Outlets + + @IBOutlet weak var nameTextField: UITextField! + @IBOutlet weak var dueTextField: UITextField! + @IBOutlet weak var notesTextView: UITextView! + @IBOutlet var datePicker: UIDatePicker! + + // MARK: - Constants & Variables + + var task: Task? + + override func viewDidLoad() { + super.viewDidLoad() + dueTextField.inputView = datePicker + } + + @IBAction func saveButtonTapped(_ sender: Any) { + + // Unwrap the name text field and ensure that the string is not empty + guard let name = nameTextField.text, + !name.isEmpty + else { return } + + // If we are in the detail view for an existing task, unwrap it + if let task = task { + + // Update the task + TaskController.shared.update(task, name: name, notes: notesTextView.text, due: datePicker.date) + + } else { + + // Create a new task + TaskController.shared.createTaskWith(name, notesTextView.text, datePicker.date) + } + + // Pop the detail vc off the navigation stack + self.navigationController?.popViewController(animated: true) + } + + @IBAction func datePickerChaged(_ sender: UIDatePicker) { + dueTextField.text = "\(sender.date)" + } +} diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/TaskListTableViewController.swift b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Controllers/View Controllers/TaskListTableViewController.swift similarity index 53% rename from Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/TaskListTableViewController.swift rename to Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Controllers/View Controllers/TaskListTableViewController.swift index 31a4683..22c15bf 100644 --- a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/TaskListTableViewController.swift +++ b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Controllers/View Controllers/TaskListTableViewController.swift @@ -26,28 +26,50 @@ class TaskListTableViewController: UITableViewController { let task = TaskController.shared.tasks[indexPath.row] cell.task = task + cell.delegate = self return cell } - /* + // Override to support editing the table view. - override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { + override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { + + let task = TaskController.shared.tasks[indexPath.row] + // Delete the row from the data source + TaskController.shared.delete(task) + tableView.deleteRows(at: [indexPath], with: .fade) - } else if editingStyle == .insert { - // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view - } + } } - */ - + // MARK: - Navigation - + // In a storyboard-based application, you will often want to do a little preparation before navigation override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - if segue.identifier == "" + // Check that we are firing the code for the correct segue + if segue.identifier == "toTaskDetail" { + // Cast the destination view controller as our custom detail vc + guard let destinationVC = segue.destination as? TaskDetailTableViewController, + // Grab the indexPath of the row that was selected + let indexPath = tableView.indexPathForSelectedRow + else { return } + + // Index the source of truth + let task = TaskController.shared.tasks[indexPath.row] + // Pass the task to the detail vc + destinationVC.task = task + } } - +} +extension TaskListTableViewController: SwitchTableViewCellDelegate { + + func isCompleteToggled(on cell: SwitchTableViewCell) { + guard let task = cell.task else { return } + TaskController.shared.toggleIsComplete(for: task) + cell.updateViews() + } } diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/CoreDataStack.swift b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Model/CoreDataStack.swift similarity index 100% rename from Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/CoreDataStack.swift rename to Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Model/CoreDataStack.swift diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Task + Convenience.swift b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Model/Task + Convenience.swift similarity index 100% rename from Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Task + Convenience.swift rename to Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Model/Task + Convenience.swift diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Task.xcdatamodeld/.xccurrentversion b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Model/Task.xcdatamodeld/.xccurrentversion similarity index 100% rename from Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Task.xcdatamodeld/.xccurrentversion rename to Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Model/Task.xcdatamodeld/.xccurrentversion diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Task.xcdatamodeld/Task.xcdatamodel/contents b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Model/Task.xcdatamodeld/Task.xcdatamodel/contents similarity index 100% rename from Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Task.xcdatamodeld/Task.xcdatamodel/contents rename to Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Model/Task.xcdatamodeld/Task.xcdatamodel/contents diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/AppIcon.appiconset/Contents.json b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/Contents.json b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Resources/Assets.xcassets/Contents.json similarity index 100% rename from Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/Contents.json rename to Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Resources/Assets.xcassets/Contents.json diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/complete.imageset/Contents.json b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Resources/Assets.xcassets/complete.imageset/Contents.json similarity index 100% rename from Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/complete.imageset/Contents.json rename to Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Resources/Assets.xcassets/complete.imageset/Contents.json diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/complete.imageset/complete.png b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Resources/Assets.xcassets/complete.imageset/complete.png similarity index 100% rename from Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/complete.imageset/complete.png rename to Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Resources/Assets.xcassets/complete.imageset/complete.png diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/incomplete.imageset/Contents.json b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Resources/Assets.xcassets/incomplete.imageset/Contents.json similarity index 100% rename from Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/incomplete.imageset/Contents.json rename to Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Resources/Assets.xcassets/incomplete.imageset/Contents.json diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/incomplete.imageset/incomplete.png b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Resources/Assets.xcassets/incomplete.imageset/incomplete.png similarity index 100% rename from Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Assets.xcassets/incomplete.imageset/incomplete.png rename to Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Resources/Assets.xcassets/incomplete.imageset/incomplete.png diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Base.lproj/LaunchScreen.storyboard b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Resources/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Base.lproj/LaunchScreen.storyboard rename to Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Resources/Base.lproj/LaunchScreen.storyboard diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Resources/Base.lproj/Main.storyboard b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Resources/Base.lproj/Main.storyboard new file mode 100644 index 0000000..6385c77 --- /dev/null +++ b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Resources/Base.lproj/Main.storyboard @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Info.plist b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Resources/Info.plist similarity index 100% rename from Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Info.plist rename to Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Resources/Info.plist diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/SwitchTableViewCell.swift b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/SwitchTableViewCell.swift deleted file mode 100644 index d837eef..0000000 --- a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/SwitchTableViewCell.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// SwitchTableViewCell.swift -// Task -// -// Created by Frank Martin on 2/21/19. -// Copyright © 2019 DevMtnStudent. All rights reserved. -// - -import UIKit - -class SwitchTableViewCell: UITableViewCell { - - var task: Task? - -} diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/TaskDetailTableViewController.swift b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/TaskDetailTableViewController.swift deleted file mode 100644 index 452b2d8..0000000 --- a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/TaskDetailTableViewController.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// TaskDetailTableViewController.swift -// Task -// -// Created by Frank Martin Jr on 1/22/19. -// Copyright © 2019 DevMtnStudent. All rights reserved. -// - -import UIKit - -class TaskDetailTableViewController: UITableViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - } - - // MARK: - Table view data source - - // Override to support editing the table view. - override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { - if editingStyle == .delete { - tableView.deleteRows(at: [indexPath], with: .fade) - } - } - - // MARK: - Navigation - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - } -} diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Views/SwitchTableViewCell.swift b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Views/SwitchTableViewCell.swift new file mode 100644 index 0000000..62c605d --- /dev/null +++ b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Views/SwitchTableViewCell.swift @@ -0,0 +1,50 @@ +// +// SwitchTableViewCell.swift +// Task +// +// Created by Frank Martin on 2/21/19. +// Copyright © 2019 DevMtnStudent. All rights reserved. +// + +import UIKit + +protocol SwitchTableViewCellDelegate: class { + func isCompleteToggled(on cell: SwitchTableViewCell) +} + +class SwitchTableViewCell: UITableViewCell { + + // MARK: - Outlets + + @IBOutlet weak var nameLabel: UILabel! + @IBOutlet weak var dueLabel: UILabel! + @IBOutlet weak var isCompleteButton: UIButton! + var task: Task? { + didSet { + updateViews() + } + } + weak var delegate: SwitchTableViewCellDelegate? + + // MARK: - Actions + + @IBAction func isCompleteButtonTapped(_ sender: UIButton) { + delegate?.isCompleteToggled(on: self) + } +} + +extension SwitchTableViewCell { + + public func updateViews() { + guard let task = task else { return } + nameLabel.text = task.name + if let due = task.due { + dueLabel.text = String(describing: due) + } else { + dueLabel.isHidden = true + } + + let image: UIImage? = task.isComplete ? UIImage(named: "complete") : UIImage(named: "incomplete") + isCompleteButton.setBackgroundImage(image, for: .normal) + } +} From 63a268a51cd50a3dc5a7d747c0db81c38ae222c1 Mon Sep 17 00:00:00 2001 From: Frank Martin Date: Thu, 14 Mar 2019 16:26:14 -0600 Subject: [PATCH 3/3] Task ReadME/master ready for review --- .../Day 3/Task 1 - Part 1.md | 19 +++++++++++++---- .../TaskDetailTableViewController.swift | 17 ++++++++++++--- .../TaskListTableViewController.swift | 21 ++++++++++++++++++- .../Task/Task/Views/SwitchTableViewCell.swift | 12 +++++------ 4 files changed, 55 insertions(+), 14 deletions(-) diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task 1 - Part 1.md b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task 1 - Part 1.md index 472f578..f97ce15 100644 --- a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task 1 - Part 1.md +++ b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task 1 - Part 1.md @@ -237,7 +237,14 @@ return cell ``` ###### Passing data -Let's handle when a user taps on an existing task. First, go into your `Main.storyboard` file and give an identifier to the segue from your cell to the TaskDetailViewController; it should be something to the effect of "toTaskDetail." Next, in your `TaskListTableViewController.swift` file, uncomment the `prepareForSegue` function. Check that the segue's identifier matches the one that you just set in storyboard; if it matches, unwrap the segue's destination view controller and the table view's selected index path. Add an optional `Task` landing pad on your `TaskDetailViewController`. Index the `tasks` array on your `TaskController` and pass the correct `Task` object to your detail view controller. +Let's handle when a user taps on an existing task. Add an optional 'landing pad' of type `Task` to your `TaskDetailTableViewController` file. + +``` +// Landing pad +var task: Task? +``` + +Now that we have provided a place for our task to land, go into your `Main.storyboard` file and give an identifier to the segue from your cell to the `TaskDetailViewController`; it should be something to the effect of "toTaskDetail." Next, in your `TaskListTableViewController.swift` file, uncomment the `prepareForSegue` function. Check that the segue's identifier matches the one that you just set in storyboard; if it matches, unwrap the segue's destination view controller and the table view's selected index path. Index the `tasks` array on your `TaskController` and pass the correct `Task` object to the landing pad on your detail view controller. ``` override func prepare(for segue: UIStoryboardSegue, sender: Any?) { @@ -295,9 +302,13 @@ Let's jump back into our storyboard and put the finishing touches on the detail ###### TaskDetailTableViewController.swift 1. Drag outlets from your storyboard's view controller into your swift file. Make sure you include an outlet for the date picker. -2. Drag out an `IBAction` for your `saveBarButtonItem`. -3. In `viewDidLoad`, assign the `dueTextField's` `inputView` to your date picker. -4. In your `saveButtonTapped` function, safely unwrap the `nameTextField's` text and either a) create a new instance of `Task`, or b) update an existing task. Remember that `due` and `notes` are both optional. Also note that the data for `due` should come from your date picker, not the `dueTextField` itself. Finally, pop the view controller off the navigation stack. This should happen regardless of whether an update or creation is occuring. +2. Add an `updateViews` function to your class that 1) unwraps your `Task` landing pad and 2) updates all of the UI elements on task detail view using the unwrapped landing pad's properties. Add a `didSet` to your `Task` landing pad and call both `loadViewIfNeeded` and `updateViews` in that order. `loadViewIfNeeded` loads the view if it hasn't already been loaded; if this function is not called, your app will crash when you get to the detail view. + +Next, let's tackle saving/updating. + +1. Drag out an `IBAction` for your `saveBarButtonItem`. +2. In `viewDidLoad`, assign the `dueTextField's` `inputView` to your date picker. +3. In your `saveButtonTapped` function, safely unwrap the `nameTextField's` text and either a) create a new instance of `Task`, or b) update an existing task. Remember that `due` and `notes` are both optional. Also note that the data for `due` should come from your date picker, not the `dueTextField` itself. Finally, pop the view controller off the navigation stack. This should happen regardless of whether an update or creation is occuring. ``` // Unwrap the name text field and ensure that the string is not empty diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Controllers/View Controllers/TaskDetailTableViewController.swift b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Controllers/View Controllers/TaskDetailTableViewController.swift index 4ecf02f..401b1b5 100644 --- a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Controllers/View Controllers/TaskDetailTableViewController.swift +++ b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Controllers/View Controllers/TaskDetailTableViewController.swift @@ -19,7 +19,12 @@ class TaskDetailTableViewController: UITableViewController { // MARK: - Constants & Variables - var task: Task? + var task: Task? { + didSet { + loadViewIfNeeded() + updateViews() + } + } override func viewDidLoad() { super.viewDidLoad() @@ -27,7 +32,7 @@ class TaskDetailTableViewController: UITableViewController { } @IBAction func saveButtonTapped(_ sender: Any) { - + // Unwrap the name text field and ensure that the string is not empty guard let name = nameTextField.text, !name.isEmpty @@ -38,7 +43,6 @@ class TaskDetailTableViewController: UITableViewController { // Update the task TaskController.shared.update(task, name: name, notes: notesTextView.text, due: datePicker.date) - } else { // Create a new task @@ -52,4 +56,11 @@ class TaskDetailTableViewController: UITableViewController { @IBAction func datePickerChaged(_ sender: UIDatePicker) { dueTextField.text = "\(sender.date)" } + + func updateViews() { + guard let task = task else { return } + nameTextField.text = task.name + dueTextField.text = task.due != nil ? String(describing: task.due!) : "" + + } } diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Controllers/View Controllers/TaskListTableViewController.swift b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Controllers/View Controllers/TaskListTableViewController.swift index 22c15bf..98db18a 100644 --- a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Controllers/View Controllers/TaskListTableViewController.swift +++ b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Controllers/View Controllers/TaskListTableViewController.swift @@ -12,9 +12,13 @@ class TaskListTableViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() - } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + tableView.reloadData() + } + // MARK: - Table view data source override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { @@ -24,8 +28,13 @@ class TaskListTableViewController: UITableViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "taskCell", for: indexPath) as? SwitchTableViewCell else { return UITableViewCell() } + // Get the task that should go in the cell let task = TaskController.shared.tasks[indexPath.row] + + // Pass the task to the cell, so that the cell can update its views cell.task = task + + // Set the TaskListTableViewController as the cell's delegate cell.delegate = self return cell @@ -36,11 +45,13 @@ class TaskListTableViewController: UITableViewController { override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { + // Get the task that the user swiped on let task = TaskController.shared.tasks[indexPath.row] // Delete the row from the data source TaskController.shared.delete(task) + // Delete the row from the table view tableView.deleteRows(at: [indexPath], with: .fade) } } @@ -49,8 +60,10 @@ class TaskListTableViewController: UITableViewController { // In a storyboard-based application, you will often want to do a little preparation before navigation override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Check that we are firing the code for the correct segue if segue.identifier == "toTaskDetail" { + // Cast the destination view controller as our custom detail vc guard let destinationVC = segue.destination as? TaskDetailTableViewController, // Grab the indexPath of the row that was selected @@ -59,6 +72,7 @@ class TaskListTableViewController: UITableViewController { // Index the source of truth let task = TaskController.shared.tasks[indexPath.row] + // Pass the task to the detail vc destinationVC.task = task } @@ -68,8 +82,13 @@ class TaskListTableViewController: UITableViewController { extension TaskListTableViewController: SwitchTableViewCellDelegate { func isCompleteToggled(on cell: SwitchTableViewCell) { + // Unwrap the task from our cell guard let task = cell.task else { return } + + // Have the Task Controller toggle the isComplete property TaskController.shared.toggleIsComplete(for: task) + + // Update the cell's views cell.updateViews() } } diff --git a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Views/SwitchTableViewCell.swift b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Views/SwitchTableViewCell.swift index 62c605d..7066b98 100644 --- a/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Views/SwitchTableViewCell.swift +++ b/Afternoon Projects/Unit 2 - Core Data & Delegates/Day 3/Task/Task/Views/SwitchTableViewCell.swift @@ -19,6 +19,9 @@ class SwitchTableViewCell: UITableViewCell { @IBOutlet weak var nameLabel: UILabel! @IBOutlet weak var dueLabel: UILabel! @IBOutlet weak var isCompleteButton: UIButton! + + // MARK: - Constants & Variables + var task: Task? { didSet { updateViews() @@ -35,14 +38,11 @@ class SwitchTableViewCell: UITableViewCell { extension SwitchTableViewCell { - public func updateViews() { + func updateViews() { + guard let task = task else { return } nameLabel.text = task.name - if let due = task.due { - dueLabel.text = String(describing: due) - } else { - dueLabel.isHidden = true - } + task.due != nil ? (dueLabel.text = String(describing: task.due!)) : (dueLabel.isHidden = true) let image: UIImage? = task.isComplete ? UIImage(named: "complete") : UIImage(named: "incomplete") isCompleteButton.setBackgroundImage(image, for: .normal)