diff --git a/Task/Task.xcodeproj/project.pbxproj b/Task/Task.xcodeproj/project.pbxproj index 66f73a4..9c61f27 100644 --- a/Task/Task.xcodeproj/project.pbxproj +++ b/Task/Task.xcodeproj/project.pbxproj @@ -7,6 +7,13 @@ objects = { /* Begin PBXBuildFile section */ + DE738E6B2492D91900C27A4F /* TaskDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE738E6A2492D91900C27A4F /* TaskDetailTableViewController.swift */; }; + DE738E6D2492D95E00C27A4F /* CoreDataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE738E6C2492D95E00C27A4F /* CoreDataStack.swift */; }; + DE738E6F2492DAC000C27A4F /* Task+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE738E6E2492DAC000C27A4F /* Task+Convenience.swift */; }; + DE738E712492DBF400C27A4F /* TaskController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE738E702492DBF400C27A4F /* TaskController.swift */; }; + DE738E7324930A5900C27A4F /* DateHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE738E7224930A5900C27A4F /* DateHelpers.swift */; }; + DE738E7524931A3000C27A4F /* ButtonTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE738E7424931A3000C27A4F /* ButtonTableViewCell.swift */; }; + DED0EF2D2495553200C8E3A9 /* TaskListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DED0EF2C2495553200C8E3A9 /* TaskListTableViewController.swift */; }; FB4BD1A7237A1D88006648A7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB4BD1A6237A1D88006648A7 /* AppDelegate.swift */; }; FB4BD1A9237A1D88006648A7 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB4BD1A8237A1D88006648A7 /* SceneDelegate.swift */; }; FB4BD1AE237A1D88006648A7 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FB4BD1AC237A1D88006648A7 /* Main.storyboard */; }; @@ -16,6 +23,13 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + DE738E6A2492D91900C27A4F /* TaskDetailTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskDetailTableViewController.swift; sourceTree = ""; }; + DE738E6C2492D95E00C27A4F /* CoreDataStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStack.swift; sourceTree = ""; }; + DE738E6E2492DAC000C27A4F /* Task+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Task+Convenience.swift"; sourceTree = ""; }; + DE738E702492DBF400C27A4F /* TaskController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskController.swift; sourceTree = ""; }; + DE738E7224930A5900C27A4F /* DateHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateHelpers.swift; sourceTree = ""; }; + DE738E7424931A3000C27A4F /* ButtonTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonTableViewCell.swift; sourceTree = ""; }; + DED0EF2C2495553200C8E3A9 /* TaskListTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskListTableViewController.swift; sourceTree = ""; }; FB4BD1A3237A1D88006648A7 /* Task.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Task.app; sourceTree = BUILT_PRODUCTS_DIR; }; FB4BD1A6237A1D88006648A7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; FB4BD1A8237A1D88006648A7 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -37,6 +51,33 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + DE738E652492D68600C27A4F /* Controllers */ = { + isa = PBXGroup; + children = ( + DE738E672492D6A600C27A4F /* ViewControllers */, + DE738E662492D69F00C27A4F /* ModelControllers */, + ); + path = Controllers; + sourceTree = ""; + }; + DE738E662492D69F00C27A4F /* ModelControllers */ = { + isa = PBXGroup; + children = ( + DE738E702492DBF400C27A4F /* TaskController.swift */, + ); + path = ModelControllers; + sourceTree = ""; + }; + DE738E672492D6A600C27A4F /* ViewControllers */ = { + isa = PBXGroup; + children = ( + DE738E6A2492D91900C27A4F /* TaskDetailTableViewController.swift */, + DE738E7424931A3000C27A4F /* ButtonTableViewCell.swift */, + DED0EF2C2495553200C8E3A9 /* TaskListTableViewController.swift */, + ); + path = ViewControllers; + sourceTree = ""; + }; FB4BD19A237A1D88006648A7 = { isa = PBXGroup; children = ( @@ -56,6 +97,7 @@ FB4BD1A5237A1D88006648A7 /* Task */ = { isa = PBXGroup; children = ( + DE738E652492D68600C27A4F /* Controllers */, FB4BD1BE237A1E0B006648A7 /* Model */, FB4BD1BF237A1E11006648A7 /* Storyboards */, FB4BD1BD237A1E00006648A7 /* Resources */, @@ -78,6 +120,9 @@ isa = PBXGroup; children = ( FB4BD1AF237A1D88006648A7 /* Task.xcdatamodeld */, + DE738E6C2492D95E00C27A4F /* CoreDataStack.swift */, + DE738E6E2492DAC000C27A4F /* Task+Convenience.swift */, + DE738E7224930A5900C27A4F /* DateHelpers.swift */, ); path = Model; sourceTree = ""; @@ -164,7 +209,14 @@ files = ( FB4BD1B1237A1D88006648A7 /* Task.xcdatamodeld in Sources */, FB4BD1A7237A1D88006648A7 /* AppDelegate.swift in Sources */, + DE738E6F2492DAC000C27A4F /* Task+Convenience.swift in Sources */, + DE738E6D2492D95E00C27A4F /* CoreDataStack.swift in Sources */, + DED0EF2D2495553200C8E3A9 /* TaskListTableViewController.swift in Sources */, + DE738E6B2492D91900C27A4F /* TaskDetailTableViewController.swift in Sources */, + DE738E712492DBF400C27A4F /* TaskController.swift in Sources */, + DE738E7524931A3000C27A4F /* ButtonTableViewCell.swift in Sources */, FB4BD1A9237A1D88006648A7 /* SceneDelegate.swift in Sources */, + DE738E7324930A5900C27A4F /* DateHelpers.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Task/Task/Controllers/ModelControllers/TaskController.swift b/Task/Task/Controllers/ModelControllers/TaskController.swift new file mode 100644 index 0000000..2691fa2 --- /dev/null +++ b/Task/Task/Controllers/ModelControllers/TaskController.swift @@ -0,0 +1,73 @@ +// +// TaskController.swift +// Task +// +// Created by Connor Holland on 6/11/20. +// Copyright © 2020 Karl Pfister. All rights reserved. +// + +import Foundation +import CoreData + +class TaskController { + + static let shared = TaskController() + + //Source of truth + + var fetchedResultsController: NSFetchedResultsController + + init() { + let fetchedRequest: NSFetchRequest = Task.fetchRequest() + + fetchedRequest.sortDescriptors = [NSSortDescriptor(key: "isComplete", ascending: true), NSSortDescriptor(key: "due", ascending: true)] + + + + let resultsController = NSFetchedResultsController(fetchRequest: fetchedRequest, managedObjectContext: CoreDataStack.context, sectionNameKeyPath: "isComplete", cacheName: nil) + fetchedResultsController = resultsController + do { + try fetchedResultsController.performFetch() + } catch { + print(error.localizedDescription) + } + } + + + // MARK: - CRUD Methods + + //create + func add(taskWithName name: String, notes: String?, due: Date?) { + Task(name: name, notes: notes, due: due) + saveToPersistenceStore() + } + + //update + func update(task: Task, name: String, notes: String, due: Date) { + task.name = name + task.notes = notes + task.due = due + saveToPersistenceStore() + } + + //delete + func delete(task: Task) { + task.managedObjectContext?.delete(task) + saveToPersistenceStore() + } + + func toggleIsComplete(task: Task) { + task.isComplete = !task.isComplete + saveToPersistenceStore() + } + + + + func saveToPersistenceStore() { + do { + try CoreDataStack.context.save() + } catch { + print("There was an error in \(#function): \(error) - \(error.localizedDescription)") + } + } +} diff --git a/Task/Task/Controllers/ViewControllers/ButtonTableViewCell.swift b/Task/Task/Controllers/ViewControllers/ButtonTableViewCell.swift new file mode 100644 index 0000000..64f9e5c --- /dev/null +++ b/Task/Task/Controllers/ViewControllers/ButtonTableViewCell.swift @@ -0,0 +1,38 @@ +// +// ButtonTableViewCell.swift +// Task +// +// Created by Connor Holland on 6/11/20. +// Copyright © 2020 Karl Pfister. All rights reserved. +// + +import UIKit + +protocol ButtonTableViewCellDelegate: AnyObject { + func buttonCellButtonTapped(_ sender: ButtonTableViewCell) +} + + +class ButtonTableViewCell: UITableViewCell { + + @IBOutlet weak var primaryLabel: UILabel! + @IBOutlet weak var completeButton: UIButton! + + + + weak var delegate: ButtonTableViewCellDelegate? + + func updateButton(_ isComplete: Bool) { + let imageName = isComplete ? "complete" : "incomplete" + completeButton.setImage(UIImage(named: imageName), for: .normal) + } + + func update(withTask task: Task) { + primaryLabel.text = task.name + updateButton(task.isComplete) + } + + @IBAction func completeButtonTapped(_ sender: Any) { + delegate?.buttonCellButtonTapped(self) + } +} diff --git a/Task/Task/Controllers/ViewControllers/TaskDetailTableViewController.swift b/Task/Task/Controllers/ViewControllers/TaskDetailTableViewController.swift new file mode 100644 index 0000000..3a3a638 --- /dev/null +++ b/Task/Task/Controllers/ViewControllers/TaskDetailTableViewController.swift @@ -0,0 +1,80 @@ +// +// TaskDetailTableViewController.swift +// Task +// +// Created by Connor Holland on 6/11/20. +// Copyright © 2020 Karl Pfister. All rights reserved. +// + +import UIKit + +class TaskDetailTableViewController: UITableViewController { + + @IBOutlet weak var taskNameTextField: UITextField! + @IBOutlet weak var dueTextField: UITextField! + @IBOutlet weak var noteTextView: UITextView! + @IBOutlet var dueDatePicker: UIDatePicker! + + + var task: Task? + var dueDateValue: Date? + + + override func viewDidLoad() { + super.viewDidLoad() + updateViews() + dueTextField.inputView = dueDatePicker + + } + + //Actions + //Come back to this + @IBAction func saveButtonTapped(_ sender: Any) { + guard let name = taskNameTextField.text, !name.isEmpty, let noteText = noteTextView.text, !noteText.isEmpty, let due = dueDateValue else {return} + if let task = task { + TaskController.shared.update(task: task, name: name, notes: noteText, due: due) + } else { + TaskController.shared.add(taskWithName: name, notes: noteText, due: due) + } + navigationController?.popViewController(animated: true) + } + + + + @IBAction func cancelButtonTapped(_ sender: Any) { + navigationController?.popViewController(animated: true) + } + + @IBAction func datePickerChanged(_ sender: UIDatePicker) { + dueDateValue = dueDatePicker.date + dueTextField.text = dueDateValue?.stringValue() + } + + @IBAction func userTappedView(_ sender: UITapGestureRecognizer) { + dueTextField.resignFirstResponder() + } + + + + func updateViews() { + taskNameTextField.text = task?.name + dueTextField.text = task?.due?.stringValue() + noteTextView.text = task?.notes + self.dueDateValue = task?.due + tableView.reloadData() + } + + + // MARK: - Table view data source + + override func numberOfSections(in tableView: UITableView) -> Int { + // #warning Incomplete implementation, return the number of sections + return 3 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + // #warning Incomplete implementation, return the number of rows + return 1 + } + +} diff --git a/Task/Task/Controllers/ViewControllers/TaskListTableViewController.swift b/Task/Task/Controllers/ViewControllers/TaskListTableViewController.swift new file mode 100644 index 0000000..903978d --- /dev/null +++ b/Task/Task/Controllers/ViewControllers/TaskListTableViewController.swift @@ -0,0 +1,129 @@ +// +// TaskListTableViewController.swift +// Task +// +// Created by Connor Holland on 6/13/20. +// Copyright © 2020 Karl Pfister. All rights reserved. +// + +import UIKit +import CoreData + +class TaskListTableViewController: UITableViewController { + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + tableView.reloadData() + } + + override func viewDidLoad() { + super.viewDidLoad() + TaskController.shared.fetchedResultsController.delegate = self + + } + + // MARK: - Table view data source + + override func numberOfSections(in tableView: UITableView) -> Int { + // #warning Incomplete implementation, return the number of sections + return TaskController.shared.fetchedResultsController.sections?.count ?? 0 + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return TaskController.shared.fetchedResultsController.sections?[section].name == "1" ? "Complete" : "Incomplete" + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + // #warning Incomplete implementation, return the number of rows + return TaskController.shared.fetchedResultsController.sections?[section].numberOfObjects ?? 0 + } + + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: "taskCell", for: indexPath) as? ButtonTableViewCell else {return UITableViewCell()} + let task = TaskController.shared.fetchedResultsController.object(at: indexPath) + cell.update(withTask: task) + cell.delegate = self + return cell + } + + + // Override to support editing the table view. + override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + if editingStyle == .delete { + // Delete the row from the data source + let task = TaskController.shared.fetchedResultsController.object(at: indexPath) + TaskController.shared.delete(task: task) + //tableView.deleteRows(at: [indexPath], with: .fade) + } + } + + + + // 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 == "toTaskDetail" { + guard let indexPath = tableView.indexPathForSelectedRow, let destination = segue.destination as? TaskDetailTableViewController else {return} + let taskToSend = TaskController.shared.fetchedResultsController.object(at: indexPath) + destination.task = taskToSend + } + } +} + +extension TaskListTableViewController: ButtonTableViewCellDelegate { + func buttonCellButtonTapped(_ sender: ButtonTableViewCell) { + guard let indexPath = tableView.indexPath(for: sender) else {return} + let task = TaskController.shared.fetchedResultsController.object(at: indexPath) + TaskController.shared.toggleIsComplete(task: task) + sender.update(withTask: task) + } +} + + +extension TaskListTableViewController: NSFetchedResultsControllerDelegate { + + func controllerWillChangeContent(_ controller: NSFetchedResultsController) { + self.tableView.beginUpdates() + } + func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + self.tableView.endUpdates() + } + + func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { + switch type { + case .insert: + guard let newIndexPath = newIndexPath else {return} + tableView.insertRows(at: [newIndexPath], with: .fade) + case .delete: + guard let indexPath = indexPath else {return} + tableView.deleteRows(at: [indexPath], with: .fade) + case .move: + guard let newIndexPath = newIndexPath, let indexPath = indexPath else {return} + tableView.moveRow(at: indexPath, to: newIndexPath) + case .update: + guard let indexPath = indexPath else {return} + tableView.reloadRows(at: [indexPath], with: .fade) + @unknown default: + fatalError() + } + } + func controller(_ controller: NSFetchedResultsController, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) { + + switch type { + case .insert: + tableView.insertSections(IndexSet(integer: sectionIndex), with: .fade) + case .delete: + tableView.deleteSections(IndexSet(integer: sectionIndex), with: .fade) + case .move: + return + case .update: + break + @unknown default: + fatalError() + } + } + + +} diff --git a/Task/Task/Model/CoreDataStack.swift b/Task/Task/Model/CoreDataStack.swift new file mode 100644 index 0000000..e68c415 --- /dev/null +++ b/Task/Task/Model/CoreDataStack.swift @@ -0,0 +1,25 @@ +// +// CoreDataStack.swift +// Task +// +// Created by Connor Holland on 6/11/20. +// Copyright © 2020 Karl Pfister. All rights reserved. +// + +import Foundation +import CoreData + +enum CoreDataStack { + static let container: NSPersistentContainer = { + let container = NSPersistentContainer(name: "Task") //<--- Change to name of app + container.loadPersistentStores { (_, error) in + if let error = error { + fatalError("\(error.localizedDescription)") + } + } + return container + }() + static var context: NSManagedObjectContext { + return container.viewContext + } +} diff --git a/Task/Task/Model/DateHelpers.swift b/Task/Task/Model/DateHelpers.swift new file mode 100644 index 0000000..6d23f2c --- /dev/null +++ b/Task/Task/Model/DateHelpers.swift @@ -0,0 +1,17 @@ +// +// DateHelpers.swift +// Task +// +// Created by Connor Holland on 6/11/20. +// Copyright © 2020 Karl Pfister. All rights reserved. +// + +import Foundation + +extension Date { + func stringValue() -> String { + let formatter = DateFormatter() + formatter.dateStyle = .medium + return formatter.string(from: self) + } +} diff --git a/Task/Task/Model/Task+Convenience.swift b/Task/Task/Model/Task+Convenience.swift new file mode 100644 index 0000000..fa22517 --- /dev/null +++ b/Task/Task/Model/Task+Convenience.swift @@ -0,0 +1,20 @@ +// +// Task+Convenience.swift +// Task +// +// Created by Connor Holland on 6/11/20. +// Copyright © 2020 Karl Pfister. All rights reserved. +// + +import Foundation +import CoreData + +extension Task { + @discardableResult + convenience init (name: String, notes: String?, due: Date?) { + self.init(context: CoreDataStack.context) + self.name = name + self.notes = notes + self.due = due + } +} diff --git a/Task/Task/Model/Task.xcdatamodeld/Task.xcdatamodel/contents b/Task/Task/Model/Task.xcdatamodeld/Task.xcdatamodel/contents index 800fc6f..9111c25 100644 --- a/Task/Task/Model/Task.xcdatamodeld/Task.xcdatamodel/contents +++ b/Task/Task/Model/Task.xcdatamodeld/Task.xcdatamodel/contents @@ -1,7 +1,12 @@ - - + + + + + + + - + \ No newline at end of file diff --git a/Task/Task/Storyboards/Base.lproj/Main.storyboard b/Task/Task/Storyboards/Base.lproj/Main.storyboard index 79c6055..8485b91 100644 --- a/Task/Task/Storyboards/Base.lproj/Main.storyboard +++ b/Task/Task/Storyboards/Base.lproj/Main.storyboard @@ -1,8 +1,237 @@ - + - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +