In order to update an existing Git submodule execute: (You might need permissions from Owner to get submodules)
git submodule update --remote --merge
- First ideas
- Modify the cell to add a Start/Done button
- Modify ToDo Model
- Replace MyToDoTableViewController with ToDoViewController
- Fix Checkmark button image when is selected
- Fix Show Add New ToDo Segue
- Fix Show Details To Do Segue
- Fix Issue when you tap save button to a new ToDo object
- Fix selected Cell
- Modify Due Date Cell Constraints
- Fix Edit button
- Fix Save button after select any table view cell to show details
- Use PropertyKey struct
- Fix Date Picker Cell
- Fix dismiss keyboard when you tap Return button keyboard
- Create Layout for CalendarView and ToDoTableView
- Create CalendarView.xib and layout
- Create load calendar view
- Add ProjectName property to ToDo class and NewProjectNameViewController
- Replace to Plus sign in CalendarView
- Modify the number of Tasks
- Modify Add button according to design
- Add SegmentedControl according to new Design
- Add a row in CalendarView to show the day number
- Move hour Labels to the right
- Create a dayFilterSegmentedControl UISegmentedControl
- Create filter function for Today, Yesterday and Tomorrow
- When you tap any cell, you have to inject the correct ToDo object, according to suitable array
- When you create a new ToDo, save it correctly and show it in table view
- Change Project's name
- Write ToDoController, basic functionality.
- Fix the AddButton.swift file again
- git reset --hard ba0e0b2
- Rename project to ChronoList again
- Rename Targets to ChronoList, ChronoListTests, and ChronoListUITests
- Write ToDoController in a new brach, basic functionality, again
- Use ToDoController to save and load ToDo objects
- Create function to filter yesterday ToDos in ToDoController
- When you tap Yesterday Segmented COntrol button, update table to show yesterday ToDo
- Fix when you add and edit a ToDo object
- Improve
unwindToToDoList(sender:)
after you save a new ToDo object - Move check box button to the right
- Once you tap addButton show keyboard right away
- Move Segmented Control outside of toolbar
- In New To Do Viewcontroller, move check box to the right.
Simple To Do list app with time tracker
1. First ideas
- Clear constraints of the text field.
- Resize the text field to add Start/done button.
- Set constraints to Start/Done button.
- Alignment: vertically in container.
- Width: greater or equal to 46.
- Set constraints to text field:
- Alignment: vertically in container.
- Horizontal space constraint: Constant 8 (from the textField to checkMarkButton, and from the text field to the StartButton.
- Build and run.
Current:
var title: String
var isComplete: Bool
var dueDate: Date
var note: String?
Which are the requirements ?
- you have to store the due date, which you already have
- I want to store the hour I stared the ToDo. We can name it: startDate
- I want to store the hour I finish the ToDo. We can name it: finishDate.
- If startDate and finishDate are not in the same dueDate, you can't do the ToDo activity. You will not able to do it, You have to re schedule. This activity have to be registered as a non acomplished activity. It would be bad for you if this happens. What you plan, you have to do it.
I think if we add this properties I can calculate the performance of a week
var title: String
var isComplete: Bool
var dueDate: Date
var startDate: Date
var finishDate: Date
var note: String?
As when you create a new ToDo Activity you don't need to fill the startDate and finishDate, you can declare both properties as optionals. If you declared as stored properties, it throws a compile error. So,
var title: String
var isComplete: Bool
var dueDate: Date
var startDate: Date?
var finishDate: Date?
var note: String?
Build and run, it is ok!
- Delete MyTodoTableViewController.
- Conect ToDoViewController with ToDoVieController swift file. (all Outlets and subclass)
- Drag and drop a new TableViewCell into the TableView.
- Connect cell with custom cell: ToDoTableViewCell
- Register the xib file for the cell and create the cell in appropiate Table View Data source method. Create the segues to trigger the NewToDoTableViewController.
Remember to configure Button State: Default
Configure button state: Selected
- I have to delete the navigation controller.
- Embed NewToDoViewController into new navigation controller.
- Drag and drop from + button to navigation controller.
- Set identifier to: ShowAddNewToDo.
- Finally, keep the same piece of code in
prepare(forSegue:sender:)
method:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showDetails" {
let toDoViewController = segue.destination as! NewToDoViewController
let indexPath = tableView.indexPathForSelectedRow!
let selectedTodo = toDos[indexPath.row]
toDoViewController.toDo = selectedTodo
}
}
Build and run.
- Drag and drop from the table view cell to the NewToDoViewController directly. (no passing through navigation controller) Like in the image, select Show.
- When you did select the cell, perform the segue:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "ShowDetailsToDo", sender: self)
}
So, now when you tap, the segue is trigger easily.
The segue identifier was missing. So you have to set again: SaveUnwind
Then you can save the ToDo object again.
When you select a cell you show the AddNewTodoViewController, when it dismissed, the cell has to be deselected.
The solution is to call the instance method deselectedRow(at:,animated:)
of the tableView:
tableView.deselectRow(at: selectedIndexPath, animated: true)
You have to call it in the prepare(for segue:,sender:)
method:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowDetailsToDo" {
let toDoViewController = segue.destination as! NewToDoViewController
let selectedIndexPath = tableView.indexPathForSelectedRow!
tableView.deselectRow(at: selectedIndexPath, animated: true)
let selectedToDo = toDos[selectedIndexPath.row]
toDoViewController.toDo = selectedToDo
}
}
I tried to do it in
didSelecteIndext(at indexPath:)
but this method clears the indexPathForSelectedRow property, and this property is required inprepare(for segue:,sender)
method, so, the application crashes when it tries to access the property.
Build and run. Now the cell is deselected after you tap any cell.
The actual Due Date Cell has a stack view in its container. Suppress this horizontal stack view, because I think it is too complex to use when the cell only has to subviews (2 labels).
Before:
After:
11. Fix Edit button
-
**Given It displays to do table view controller
-
**When I tap Edit button
-
**Then Display Delete button correctly, and set editing mode
-
**Given It displays to do table view controller.
-
**When I tap Done button
-
**Then Display Edit button correctly, and unset editing mode.
- Drag and drop UIBarButtonItem object, name it editBarItemButton set System Item to Custom.
- Create an IBOutlet for editBarItemButton
@IBOutlet weak var editBarButtonItem: UIBarButtonItem!
- Set bar button item title to "Edit" in
viewDidLoad()
because as it is a custom item, it needs suitable title
editBarButtonItem.title = "Edit"
- Create an IBAction for editBarButtonItemTapped and do the logic:
@IBAction func editBarButtonItemTapped(_ sender: UIBarButtonItem) {
if !tableView.isEditing {
tableView.setEditing(true, animated: true)
sender.title = "Done"
} else {
tableView.setEditing(false, animated: true)
sender.title = "Edit"
}
}
- Do not forget connect the action.
- Build and run.
- **Given I am displaying Details of a note.
- **When I tap Save button
- **Then save actual toDo object and do not duplicate.
- Reviewing the piece of code that saves the toDo instance, I did find that I was clearing the property indexPathForSelectedRow when I was preparing the segue, so the flow of the program was not updating the toDo instance. This did duplicate the toDo instance. To fix this, first remove the line where you were clearing the property, this was located in
prepare(forSegue:)
method. - Modify
unwindToToDoList(segue:)
method, replace the logic with a switch case statement.
@IBAction func unwindToToDoList(segue: UIStoryboardSegue) {
switch segue.identifier {
case "SaveUnwind":
let sourceViewController = segue.source as! NewToDoViewController
if let toDo = sourceViewController.toDo {
if let selectedIndexPath = tableView.indexPathForSelectedRow {
toDos[selectedIndexPath.row] = toDo
tableView.reloadRows(at: [selectedIndexPath], with: .none)
tableView.deselectRow(at: selectedIndexPath, animated: true)
} else {
let newIndexPath = IndexPath(row: toDos.count, section: 0)
toDos.append(toDo)
tableView.insertRows(at: [newIndexPath], with: .automatic)
}
ToDo.saveToDos(toDos)
}
case "CancelUnwind":
if let selectedIndexPath = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: selectedIndexPath, animated: true)
}
default:
preconditionFailure("No segue identified")
}
}
- Verify the CancelUnwind identifier correspond to the segue from Cancel Button to Exit object.
- Build and run.
- Use it in NewToDoTableViewController
struct PropertyKeys {
static let SaveUnwind = "SaveUnwind"
}
- Use it in ToDoTableViewController
struct PropertyKeys {
static let ToDoCellIdentifier = "ToDoCellIdentifier"
static let ShowDetailsToDo = "ShowDetailsToDo"
static let SaveUnwind = "SaveUnwind"
static let CancelUnwind = "CancelUnwind"
}
Build and run again.
- Make Conform ToDoViewController to UITextFieldDelegate protocol
class NewToDoViewController: UITableViewController, UITextFieldDelegate {
- Implement
textFieldShouldReturn(textField:)
delegate method, and make textField to resign first responder:
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
}
- Build and run. Test.
- Clear tableView constraints.
- Embed tableView into vertical stack view.
- Drag and drop an UIView object.
- Set Distribution to Fill Equally,
- Set vertical stack view constraints to trail = 0, leading = 0, top = 0 and bottom to 0.
- In toolbar object, delete the space, the add button will be placed at the left corner.
- Build and run.
Fist attempt
- Make CalendarView File's owner ToDoViewController
- Create an IBOutlet in ToDoViewController.swift
@IBOutlet weak var calendarView: UIView!
- Make sure to connect the outlet inside CalendarView.xib as in the figure:
- Load calendar view in
viewDidLoad()
method:
loadCalendarView()
Then:
Bundle.main.loadNibNamed("CalendarView", owner: self, options: nil)
calendarView.frame.origin = CGPoint(x: 0, y: 55)
view.addSubview(calendarView)
Build and run.
import Foundation
struct ToDo: Codable {
var projectName: String
var title: String
var isComplete: Bool
var dueDate: Date
var startDate: Date?
var finishDate: Date?
var note: String?
static let DocumentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
static let ArchiveURL = DocumentsDirectory.appendingPathComponent("todos").appendingPathExtension("plist")
init(projectName: String, title: String, isComplete: Bool, dueDate: Date, note: String?)
{
self.projectName = projectName
self.title = title
self.isComplete = isComplete
self.dueDate = dueDate
self.note = note
}
static func loadToDos() -> [ToDo]? {
guard let codedToDos = try? Data(contentsOf: ArchiveURL) else {return nil}
let propertyListDecoder = PropertyListDecoder()
return try? propertyListDecoder.decode(Array<ToDo>.self, from: codedToDos)
}
static func saveToDos(_ toDos:[ToDo]) {
let propertyListEncoder = PropertyListEncoder()
let codedToDos = try? propertyListEncoder.encode(toDos)
try? codedToDos?.write(to: ArchiveURL, options: .noFileProtection)
}
static func loadSampleToDos() -> [ToDo] {
let todo1 = ToDo(projectName: "p1", title: "ToDo 1", isComplete: false, dueDate: Date(), note: "Sample Note 1")
let todo2 = ToDo(projectName: "p1", title: "ToDo 2", isComplete: false, dueDate: Date(), note: "Sample Note 2")
let todo3 = ToDo(projectName: "p1", title: "ToDo 3", isComplete: false, dueDate: Date(), note: "Sample Note 3")
let todo4 = ToDo(projectName: "p1", title: "ToDo 4", isComplete: false, dueDate: Date(), note: "Sample Note 4")
let todo5 = ToDo(projectName: "p1", title: "ToDo 5", isComplete: false, dueDate: Date(), note: "Sample Note 5")
return [todo1, todo2, todo3, todo4, todo5]
}
static let dueDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .short
return formatter
}()
}
Modify temporary prepareForSegue
method in NewToDoTableViewController:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
guard segue.identifier == PropertyKeys.SaveUnwind else { return }
let title = titleTextField.text!
let isComplete = isCompletButton.isSelected
let dueDate = dueDatePickerView.date
let note = notesTextView.text
toDo = ToDo(projectName: "p1", title: title, isComplete: isComplete, dueDate: dueDate, note: note)
}
Create AddProjectNameViewController:
import UIKit
class AddProjectNameViewController: UIViewController, UITextFieldDelegate {
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var textField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
registerForKeyNotification()
}
func registerForKeyNotification() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWasShown(_:)), name: NSNotification.Name.UIKeyboardDidShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillBeHidden(_:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
@objc func keyboardWasShown(_ notification: NSNotification) {
guard let info = notification.userInfo, let keyboardFrameValue = info[UIKeyboardFrameBeginUserInfoKey] as? NSValue else { return }
let keyboardFrame = keyboardFrameValue.cgRectValue
let tableViewWithKeyboardContentInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: keyboardFrame.size.height, right: 0.0)
tableView.contentInset = tableViewWithKeyboardContentInsets
}
@objc func keyboardWillBeHidden(_ notification: NSNotification) {
let contentInsets = UIEdgeInsets.zero
tableView.contentInset = contentInsets
tableView.scrollIndicatorInsets = contentInsets
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
}
}
Build and run.
At this stage, Xcode 12.2 was released so I had to deployed the target to iOS 14.2
Use AddButton.swift file to customize each button.
import UIKit
@IBDesignable
class AddButton: UIButton {
@IBInspectable var fillColor: UIColor = UIColor.orange
private var halfWidth: CGFloat {
return bounds.width / 2
}
private var halfHeight: CGFloat {
return bounds.height / 2
}
override func draw(_ rect: CGRect) {
let bezierPath = UIBezierPath(ovalIn: rect)
fillColor.setFill()
bezierPath.fill()
let plusWidth: CGFloat = min(bounds.width, bounds.height)*0.6
let halfPlusWidth = plusWidth / 2
let plusPath = UIBezierPath()
plusPath.lineWidth = 4.0
plusPath.move(to: CGPoint(x: halfWidth - halfPlusWidth, y: halfHeight))
plusPath.addLine(to: CGPoint(x: halfWidth + halfPlusWidth, y: halfHeight))
plusPath.move(to: CGPoint(x: halfWidth, y: halfHeight-halfPlusWidth))
plusPath.addLine(to: CGPoint(x: halfWidth, y: halfHeight+halfPlusWidth))
UIColor.white.setStroke()
plusPath.stroke()
}
func pulsate() {
let pulse = CASpringAnimation(keyPath: "transform.scale")
pulse.duration = 0.5
pulse.fromValue = 0.98
pulse.toValue = 1.1
pulse.initialVelocity = 1.0
pulse.damping = 1.0
layer.add(pulse, forKey: nil)
}
}
Build and run.
As you know, day has 8 official hours to work, modify CalendarView to show 8 hours plus 2 hours to eat.
- As you know, you used Stack views to create CalendarView, You just copy/paste a row to increase the number of tasks and change the labels corresponding to the hour.
- Build and run.
At this stage, The render and update auto layout failed.
The new design paints an outside ring, in the center paints a plus sign.
- First paint the outside ring:
let center = CGPoint(x: bounds.width/2, y: bounds.height/2)
let radius: CGFloat = min(bounds.width/2, bounds.height/2)
let startAngle: CGFloat = 0.0
let endAngle: CGFloat = .pi*2
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.lineWidth = 2.0
plusColor.setStroke()
path.stroke()
- Second, it paints the plus sign
let plusWidth: CGFloat = min(bounds.width, bounds.height)*0.6
let halfPlusWidth = plusWidth/2
let plusPath = UIBezierPath()
plusPath.lineWidth = 3.0
plusPath.move(to: CGPoint(x: halfWidth - halfPlusWidth, y: halfHeight))
plusPath.addLine(to: CGPoint(x: halfWidth + halfPlusWidth, y: halfHeight))
plusPath.move(to: CGPoint(x: halfWidth, y: halfHeight-halfPlusWidth))
plusPath.addLine(to: CGPoint(x: halfWidth, y: halfHeight+halfPlusWidth))
UIColor.black.setStroke()
plusColor.setStroke()
plusPath.stroke()
- Build and run:
- This time it paints correctly the size of the button, It was solve changing the size of the radious:
let radius: CGFloat = min(bounds.width/2, bounds.height/2)
According to new design, insert an segmented control to the toolbar, then you are going to show Yesterday, tomorrow and today controls. Remove the add button on toolbar.
We are going to use segmented control to filter toDo list by date (yesterday, today and tomorrow). We also add a Segmented control to Last, Current and Next Week:
You just move the hour label to the lowest place in the stack view.
- In ToDoViewController declare an UISegmentedControl object and connect it to canvas. This would filter an specific day of the week, in this case we will filter today, yesterday and tomorrow.
@IBOutlet weak var dayFilterSegmentedControl: UISegmentedControl!
It is not needed at this stage, but it is done !
- In ToDoViewController create an IBAction for the segmented control UISegmentedControl and connect it to canvas, act.
@IBAction func filterOptionUpdated(_ sender: UISegmentedControl) {
}
- Create a method to create a new array of toDos that correspond the day and connect it to canvas:
@IBAction func filterOptionUpdated(_ sender: UISegmentedControl) {
updateMatchingToDos()
}
- Write updateMatchingToDos method, first, we need empty the toDos array and reload data each time the segmented control change.
func updateMatchingToDos() {
toDos = []
self.tableView.reloadData()
}
- Create array of day options:
let dayOptions = ["Yesterday", "Today", "Tomorrow"];
- Create day option variable, initialize to "Yesterday"
var dayOption: String = "Yesterday"
- Create an global instance of Calendar:
let calendar: Calendar = {
var calendar = Calendar.current
calendar.timeZone = TimeZone.current
return calendar
}()
- Create a function loadToDos(), in this function load toDos in order if there are saved toDos or if there is no saved todos, load them from samples. After that, you have to checked the value of dayOption according to selected Segmented index and then filter each toDo with its corresponding array.
func loadToDos() {
if let unwrappedToDos = ToDo.loadToDos() {
toDos = unwrappedToDos
} else {
toDos = ToDo.loadSampleToDos()
}
dayOption = dayOptions[dayFilterSegmentedControl.selectedSegmentIndex]
switch dayOption {
case "Today":
todayToDos = filterToDosFromToday(toDos: toDos)
case "Yesterday":
yesterdayToDos = filterToDosFromYesterday(toDos: toDos)
case "Tomorrow":
tomorrowToDos = filterToDosFromTomorrow(toDos: toDos)
case "All":
allToDos = toDos
default:
allToDos = toDos
}
}
- Each time you tap the segmentd control you have to update the number of rows in section and the cell for row at indexPath:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
dayOption = dayOptions[dayFilterSegmentedControl.selectedSegmentIndex];
switch dayOption {
case "Today":
return todayToDos.count
case "Yesterday":
return yesterdayToDos.count
case "Tomorrow":
return tomorrowToDos.count
default:
return toDos.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: PropertyKeys.ToDoCellIdentifier) as? ToDoTableViewCell else { fatalError("Could not dequeue a cell") }
cell.delegate = self
dayOption = dayOptions[dayFilterSegmentedControl.selectedSegmentIndex];
switch dayOption {
case "Today":
let toDo = todayToDos[indexPath.row]
cell.titleTextField?.text = toDo.title
cell.isCompleteButton.isSelected = toDo.isComplete
case "Yesterday":
let toDo = yesterdayToDos[indexPath.row]
cell.titleTextField?.text = toDo.title
cell.isCompleteButton.isSelected = toDo.isComplete
case "Tomorrow":
let toDo = tomorrowToDos[indexPath.row]
cell.titleTextField?.text = toDo.title
cell.isCompleteButton.isSelected = toDo.isComplete
default:
let toDo = toDos[indexPath.row]
cell.titleTextField?.text = toDo.title
cell.isCompleteButton.isSelected = toDo.isComplete
}
return cell
}
- Update
updateMatchingToDos
method:
func updateMatchingToDos() {
dayOption = dayOptions[dayFilterSegmentedControl.selectedSegmentIndex];
switch dayOption {
case "Today":
todayToDos = self.filterToDosFromToday(toDos: toDos)
case "Yesterday":
yesterdayToDos = self.filterToDosFromYesterday(toDos: toDos)
case "Tomorrow":
tomorrowToDos = self.filterToDosFromTomorrow(toDos: toDos)
case "All":
allToDos = toDos
default:
toDos = []
}
self.tableView.reloadData()
}
- Create each method to filter suitable array of ToDos
func filterToDosFromToday(toDos: [ToDo]) -> [ToDo] {
todayToDos = []
for toDo in toDos {
if calendar.isDateInToday(toDo.dueDate) {
todayToDos.append(toDo)
}
}
return todayToDos
}
func filterToDosFromYesterday(toDos: [ToDo]) -> [ToDo] {
yesterdayToDos = []
for toDo in toDos {
if calendar.isDateInYesterday(toDo.dueDate) {
yesterdayToDos.append(toDo)
}
}
return yesterdayToDos
}
func filterToDosFromTomorrow(toDos: [ToDo]) -> [ToDo] {
tomorrowToDos = []
for toDo in toDos {
if calendar.isDateInTomorrow(toDo.dueDate) {
tomorrowToDos.append(toDo)
}
}
return tomorrowToDos
}
- Remember to declare global array variables to hold suitable values, Use ToDo.swift file
var toDos = [ToDo]()
var todayToDos = [ToDo]()
var yesterdayToDos = [ToDo]()
var tomorrowToDos = [ToDo]()
var allToDos = [ToDo]()
- Build and run. Each time you tap the segmented control, you can see filtered each group of arrays:
There is a problem when you tap any cell, the object that is injected in NewTodoViewController is not suitable. To fix this, you have to pass the correct object instance. In prepare(segue:,sender:)
method, inject the correct object instance:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == PropertyKeys.ShowDetailsToDo {
let toDoViewController = segue.destination as! NewToDoViewController
let selectedIndexPath = tableView.indexPathForSelectedRow!
dayOption = dayOptions[dayFilterSegmentedControl.selectedSegmentIndex];
var selectedToDo: ToDo
switch dayOption {
case "Today":
selectedToDo = todayToDos[selectedIndexPath.row]
case "Yesterday":
selectedToDo = yesterdayToDos[selectedIndexPath.row]
case "Tomorrow":
selectedToDo = tomorrowToDos[selectedIndexPath.row]
case "All":
selectedToDo = toDos[selectedIndexPath.row]
default:
preconditionFailure("No day option value")
}
toDoViewController.toDo = selectedToDo
}
}
The issue was solved modifying unwindToToDoList(segue:)
, just right after you check that there is no selectedIndexPath:
@IBAction func unwindToToDoList(segue: UIStoryboardSegue) {
switch segue.identifier {
case PropertyKeys.SaveUnwind:
let sourceViewController = segue.source as! NewToDoViewController
if let toDo = sourceViewController.toDo {
if let selectedIndexPath = tableView.indexPathForSelectedRow {
toDos[selectedIndexPath.row] = toDo
tableView.reloadRows(at: [selectedIndexPath], with: .none)
tableView.deselectRow(at: selectedIndexPath, animated: true)
} else {
var newIndexPath: IndexPath
dayOption = dayOptions[dayFilterSegmentedControl.selectedSegmentIndex]
if dayOption == day(dueDate: toDo.dueDate) {
switch dayOption {
case "Today":
newIndexPath = IndexPath(row: todayToDos.count, section: 0)
todayToDos.append(toDo)
case "Yesterday":
newIndexPath = IndexPath(row: yesterdayToDos.count, section: 0)
yesterdayToDos.append(toDo)
case "Tomorrow":
newIndexPath = IndexPath(row: tomorrowToDos.count, section: 0)
tomorrowToDos.append(toDo)
case "All":
newIndexPath = IndexPath(row: allToDos.count, section: 0)
allToDos.append(toDo)
default:
preconditionFailure("No day option")
}
tableView.insertRows(at: [newIndexPath], with: .automatic)
}
toDos.append(toDo)
}
ToDo.saveToDos(toDos)
}
case PropertyKeys.CancelUnwind:
if let selectedIndexPath = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: selectedIndexPath, animated: true)
}
default:
preconditionFailure("No segue identified")
}
}
After a while, I created a documentation project to remember the steps to change Project's name in Xcode
import Foundation
enum SavingToDoResult {
case success
case failure(Error)
case failureCreatingEncoder(Error)
case failureWritingData(Error)
}
enum URLError: Error {
case invalidURL
}
enum DataError: Error {
case invalidData
}
enum LoadingToDoResult {
case success([ToDo]?)
case failureInvalidURL(URLError)
case failureInvalidData(DataError)
}
class ToDoController {
static let shared = ToDoController()
static let DocumentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
static let ArchiveURL = DocumentsDirectory.appendingPathComponent("todos").appendingPathExtension("plist")
func loadToDos(completion: @escaping (LoadingToDoResult) -> Void) {
var codedToDos: Data
do {
codedToDos = try Data(contentsOf: ToDoController.ArchiveURL)
} catch {
return completion(.failureInvalidURL(.invalidURL))
}
do {
let propertyListDecoder = PropertyListDecoder()
let toDos = try propertyListDecoder.decode(Array<ToDo>.self, from: codedToDos)
return completion(.success(toDos))
} catch {
return completion(.failureInvalidData(.invalidData))
}
}
func saveToDos(toDos: [ToDo]) -> SavingToDoResult {
let propertyListEncoder = PropertyListEncoder()
var codedToDos: Data
do {
codedToDos = try propertyListEncoder.encode(toDos)
} catch let error {
return .failureCreatingEncoder(error)
}
do {
try codedToDos.write(to: ToDoController.ArchiveURL, options: .noFileProtection)
return .success
} catch let error {
return .failureWritingData(error)
}
}
static func loadSampleToDos() -> [ToDo] {
let todo1 = ToDo(projectName: "para antier", title: "Para antier", isComplete: false, dueDate: Date().addingTimeInterval(-2*(24*60*60)), note: "para antier")
let todo2 = ToDo(projectName: "para ayer", title: "para ayer", isComplete: false, dueDate: Date().addingTimeInterval(-24*60*60), note: "Sample Note 2")
let todo3 = ToDo(projectName: "para hoy", title: "Para Hoy", isComplete: false, dueDate: Date(), note: "Sample Note 3")
let todo4 = ToDo(projectName: "para mañana", title: "Para mañana", isComplete: false, dueDate: Date().addingTimeInterval(24*60*60), note: "Sample Note 4")
let todo5 = ToDo(projectName: "para pasado mañana", title: "para pasado mañana", isComplete: false, dueDate: Date().addingTimeInterval(2*24*60*60), note: "Sample Note 5")
return [todo1, todo2, todo3, todo4, todo5]
}
}
Xcode couldn't find AddButton.swift class because according to Xcode was located in another disk, fatal error because the file was in another hardisk that was unmounted, I had to look in documentations Modify Add button according to design to rewrite the code. Again, I put the solution here to document again.:
AddButton.swift file:
import UIKit
@IBDesignable
class AddButton: UIButton {
@IBInspectable var fillColor: UIColor = UIColor.orange
private var halfWidth: CGFloat {
return bounds.width / 2
}
private var halfHeight: CGFloat {
return bounds.height / 2
}
override func draw(_ rect: CGRect) {
// first paint the outside ring
let center = CGPoint(x: bounds.width/2, y: bounds.height/2)
let radius: CGFloat = min(bounds.width/2, bounds.height/2)
let startAngle: CGFloat = 0.0
let endAngle: CGFloat = .pi*2
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.lineWidth = 2.0
fillColor.setStroke()
path.stroke()
// Second, it paints the plus sign
let plusWidth: CGFloat = min(bounds.width, bounds.height)*0.6
let halfPlusWidth = plusWidth/2
let plusPath = UIBezierPath()
plusPath.lineWidth = 3.0
plusPath.move(to: CGPoint(x: halfWidth - halfPlusWidth, y: halfHeight))
plusPath.addLine(to: CGPoint(x: halfWidth + halfPlusWidth, y: halfHeight))
plusPath.move(to: CGPoint(x: halfWidth, y: halfHeight-halfPlusWidth))
plusPath.addLine(to: CGPoint(x: halfWidth, y: halfHeight+halfPlusWidth))
UIColor.black.setStroke()
fillColor.setStroke()
plusPath.stroke()
}
func pulsate() {
let pulse = CASpringAnimation(keyPath: "transform.scale")
pulse.duration = 0.5
pulse.fromValue = 0.98
pulse.toValue = 1.1
pulse.initialVelocity = 1.0
pulse.damping = 1.0
layer.add(pulse, forKey: nil)
}
}
Reset project to a version where the ToDo objects were saved in one only array. I also fixed the missing AddButton.swift file.
Remember to schedule new commits to rewrite important things you were doing. (ToDoController, rename Project).
Follow the steps to rename projects in Xcode link
Follow the steps to rename targets, do not forget to document the new Info.plist file path to successfully change the target's name.
In this swift file, you are going to create an object that is in charge of share a single instance of the object. Load and save ToDo objects.
import Foundation
enum SavingToDoResult {
case success
case failure(Error)
case failureCreatingEncoder(Error)
case failureWritingData(Error)
}
enum URLError: Error {
case invalidURL
}
enum DataError: Error {
case invalidData
}
enum LoadingToDoResult {
case success([ToDo]?)
case failureInvalidURL(URLError)
case failureInvalidData(DataError)
}
class ToDoController {
static let shared = ToDoController()
static let DocumentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
static let ArchiveURL = DocumentsDirectory.appendingPathComponent("todos").appendingPathExtension("plist")
func loadToDos(completion: @escaping (LoadingToDoResult) -> Void) {
var codedToDos: Data
do {
codedToDos = try Data(contentsOf: ToDoController.ArchiveURL)
} catch {
return completion(.failureInvalidURL(.invalidURL))
}
do {
let propertyListDecoder = PropertyListDecoder()
let toDos = try propertyListDecoder.decode(Array<ToDo>.self, from: codedToDos)
return completion(.success(toDos))
} catch {
return completion(.failureInvalidData(.invalidData))
}
}
func saveToDos(toDos: [ToDo]) -> SavingToDoResult {
let propertyListEncoder = PropertyListEncoder()
var codedToDos: Data
do {
codedToDos = try propertyListEncoder.encode(toDos)
} catch let error {
return .failureCreatingEncoder(error)
}
do {
try codedToDos.write(to: ToDoController.ArchiveURL, options: .noFileProtection)
return .success
} catch let error {
return .failureWritingData(error)
}
}
static func loadSampleToDos() -> [ToDo] {
let todo1 = ToDo(projectName: "para antier", title: "Para antier", isComplete: false, dueDate: Date().addingTimeInterval(-2*(24*60*60)), note: "para antier")
let todo2 = ToDo(projectName: "para ayer", title: "para ayer", isComplete: false, dueDate: Date().addingTimeInterval(-24*60*60), note: "Sample Note 2")
let todo3 = ToDo(projectName: "para hoy", title: "Para Hoy", isComplete: false, dueDate: Date(), note: "Sample Note 3")
let todo4 = ToDo(projectName: "para mañana", title: "Para mañana", isComplete: false, dueDate: Date().addingTimeInterval(24*60*60), note: "Sample Note 4")
let todo5 = ToDo(projectName: "para pasado mañana", title: "para pasado mañana", isComplete: false, dueDate: Date().addingTimeInterval(2*24*60*60), note: "Sample Note 5")
return [todo1, todo2, todo3, todo4, todo5]
}
}
After learn how to use enumerations, I am able to use them in ToDoViewController.swift file. First I had to modify load and save toDos methods:
First version of load toDos:
func prototypeMark1LoadToDos(completion: @escaping (LoadToDoResult) -> Void) {
var codedToDos: Data
do {
codedToDos = try Data(contentsOf: ToDoController.ArchiveURL)
} catch let error {
return completion(.failureInvalidURL(error as! URLError))
}
let propertyListDecoder = PropertyListDecoder()
if let toDos = try? propertyListDecoder.decode(Array<ToDo>.self, from: codedToDos) {
return completion(.success(toDos))
} else {
return completion(.failureInvalidData("System couldn't decode"))
}
}
Second version of load toDos:
func prototypeMark2LoadToDos() -> [ToDo]? {
var codedToDos: Data
do {
codedToDos = try Data(contentsOf: ToDoController.ArchiveURL)
} catch {
return nil
}
do {
let propertyListDecoder = PropertyListDecoder()
let toDos = try propertyListDecoder.decode(Array<ToDo>.self, from: codedToDos)
return toDos
} catch {
return nil
}
}
I used first version in ToDoViewController.swift file:
First in viewDidLoad()
:
override func viewDidLoad() {
tableView.delegate = self
tableView.register(UINib(nibName: "ToDoTableViewCell", bundle: nil), forCellReuseIdentifier: "ToDoCellIdentifier")
editBarButtonItem.title = "Edit"
ToDoController.shared.prototypeMark1LoadToDos { (LoadToDoResult) in
switch LoadToDoResult {
case .success(let obteinedToDos):
toDos = obteinedToDos
case .failureInvalidData(let message):
print("\(message)")
toDos = ToDoController.loadSampleToDos()
case .failureInvalidURL(let urlError):
print("\(urlError.localizedDescription)")
toDos = ToDoController.loadSampleToDos()
}
}
loadCalendarView()
}
Second in delegate method tableView(_ tableView:,editingStyle:,indexPath:)
:
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let title = "Delete"
let name = toDos[indexPath.row].title
let messageFormatted = String(format: "delete: \(name) ?", name)
let alertController = UIAlertController(title: title, message: messageFormatted, preferredStyle: .actionSheet)
let destructiveAction = UIAlertAction(title: "Delete", style: .destructive) { (_) in
toDos.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
switch ToDoController.shared.saveToDos(toDos: toDos) {
case .success:
print("success writing data after editing")
case .failureWritingData(let error):
print("failure writing data, \(error)", error.localizedDescription)
case .failureCreatingEncoder(let error):
print("failure creating encoder, \(error)", error.localizedDescription)
}
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alertController.addAction(destructiveAction)
alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil)
}
}
Third in unwindToToDoList(segue:)
:
@IBAction func unwindToToDoList(segue: UIStoryboardSegue) {
switch segue.identifier {
case PropertyKeys.SaveUnwind:
let sourceViewController = segue.source as! NewToDoViewController
if let toDo = sourceViewController.toDo {
if let selectedIndexPath = tableView.indexPathForSelectedRow {
toDos[selectedIndexPath.row] = toDo
tableView.reloadRows(at: [selectedIndexPath], with: .none)
tableView.deselectRow(at: selectedIndexPath, animated: true)
} else {
let newIndexPath = IndexPath(row: toDos.count, section: 0)
toDos.append(toDo)
tableView.insertRows(at: [newIndexPath], with: .automatic)
}
switch ToDoController.shared.saveToDos(toDos: toDos) {
case .success:
print("success writing data after edit complete object")
case .failureWritingData(let error):
print("failure writing data, \(error)", error.localizedDescription)
case .failureCreatingEncoder(let error):
print("failure creating encoder, \(error)", error.localizedDescription)
}
}
case PropertyKeys.CancelUnwind:
if let selectedIndexPath = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: selectedIndexPath, animated: true)
}
default:
preconditionFailure("No segue identified")
}
}
Finally in checkmarkTapped(sender:)
:
func checkmarkTapped(sender: ToDoTableViewCell) {
if let indexPath = tableView.indexPath(for: sender) {
var toDo = toDos[indexPath.row]
toDo.isComplete = !toDo.isComplete
toDos[indexPath.row] = toDo
tableView.reloadRows(at: [indexPath], with: .automatic)
switch ToDoController.shared.saveToDos(toDos: toDos) {
case .success:
print("success writing data after tap checkmark")
case .failureWritingData(let error):
print("failure writing data, \(error)", error.localizedDescription)
case .failureCreatingEncoder(let error):
print("failure creating encoder, \(error)", error.localizedDescription)
}
}
}
After a few test, the save and load methods work correctly:
1, Create an enumeration with the option day:
enum OptionDay {
case yesterday, today, tomorrow
}
- In order to get a filtered list of ToDo, create a function in ToDoController, this would help us to filter for each option day:
func toDosFor(optionDay: OptionDay, toDos: [ToDo]) -> [ToDo] {
var selectedTodos: [ToDo] = []
let calendar: Calendar = {
var calendar = Calendar.current
calendar.timeZone = TimeZone.current
return calendar
}()
switch optionDay {
case .today:
for toDo in toDos {
if calendar.isDateInToday(toDo.dueDate) {
selectedTodos.append(toDo)
}
}
case .tomorrow:
for toDo in toDos {
if calendar.isDateInTomorrow(toDo.dueDate) {
selectedTodos.append(toDo)
}
}
case .yesterday:
for toDo in toDos {
if calendar.isDateInYesterday(toDo.dueDate) {
selectedTodos.append(toDo)
}
}
}
return selectedTodos
}
- Use this function right away after get the complete ToDo objects in `viewDidLoad()
override func viewDidLoad() {
tableView.delegate = self
tableView.register(UINib(nibName: "ToDoTableViewCell", bundle: nil), forCellReuseIdentifier: "ToDoCellIdentifier")
editBarButtonItem.title = "Edit"
ToDoController.shared.prototypeMark1LoadToDos { (LoadToDoResult) in
switch LoadToDoResult {
case .success(let obteinedToDos):
self.toDos = ToDoController.shared.toDosFor(optionDay: .today, toDos: obteinedToDos) // filter ToDo
case .failureInvalidData(let message):
print("\(message)")
self.toDos = ToDoController.loadSampleToDos()
case .failureInvalidURL(let urlError):
print("\(urlError.localizedDescription)")
self.toDos = ToDoController.loadSampleToDos()
}
}
loadCalendarView()
}
Build and run, the ToDo array for today will be display.
- In ToDoViewController declare an UISegmentedControl object and connect it to canvas. This would filter an specific day of the week, in this case we will filter today, yesterday and tomorrow.
@IBOutlet weak var optionDaySegmentedControl: UISegmentedControl!
- In ToDoViewController create an IBAction for the segmented control UISegmentedControl and connect it to canvas, act.
@IBAction func toDosForSelectedDay(_ sender: UISegmentedControl) {
var updatedToDos = [ToDo]()
ToDoController.shared.prototypeMark1LoadToDos { (LoadToDoResult) in
switch LoadToDoResult {
case .success(let obteinedToDos):
updatedToDos = obteinedToDos
case .failureInvalidData(let message):
print("\(message)")
self.toDos = ToDoController.loadSampleToDos()
case .failureInvalidURL(let urlError):
print("\(urlError.localizedDescription)")
self.toDos = ToDoController.loadSampleToDos()
}
}
switch sender.selectedSegmentIndex {
case 0:
self.toDos = ToDoController.shared.toDosFor(optionDay: .yesterday, toDos: updatedToDos)
case 1:
self.toDos = ToDoController.shared.toDosFor(optionDay: .today, toDos: updatedToDos)
case 2:
self.toDos = ToDoController.shared.toDosFor(optionDay: .tomorrow, toDos: updatedToDos)
default:
preconditionFailure("There is no other option")
}
tableView.reloadData()
}
- Update viewDidLoad to set the selected segment index, also, set a toDoList variable to get the original list of ToDos loaded from disk
override func viewDidLoad() {
tableView.delegate = self
tableView.register(UINib(nibName: "ToDoTableViewCell", bundle: nil), forCellReuseIdentifier: "ToDoCellIdentifier")
editBarButtonItem.title = "Edit"
ToDoController.shared.prototypeMark1LoadToDos { (LoadToDoResult) in
switch LoadToDoResult {
case .success(let obteinedToDos):
self.toDoList = obteinedToDos
self.toDos = ToDoController.shared.toDosFor(optionDay: .today, toDos: obteinedToDos)
self.optionDaySegmentedControl.selectedSegmentIndex = 1 // set control button to show Today ToDos
case .failureInvalidData(let message):
print("\(message)")
self.toDos = ToDoController.loadSampleToDos()
case .failureInvalidURL(let urlError):
print("\(urlError.localizedDescription)")
self.toDos = ToDoController.loadSampleToDos()
}
}
loadCalendarView()
}
- After create a new ToDo object, create a flow to update the tableView:
@IBAction func unwindToToDoList(segue: UIStoryboardSegue) {
switch segue.identifier {
case PropertyKeys.SaveUnwind:
let sourceViewController = segue.source as! NewToDoViewController
if let toDo = sourceViewController.toDo {
if let selectedIndexPath = tableView.indexPathForSelectedRow {
toDos[selectedIndexPath.row] = toDo
tableView.reloadRows(at: [selectedIndexPath], with: .none)
tableView.deselectRow(at: selectedIndexPath, animated: true)
} else {
let newIndexPath = IndexPath(row: toDos.count, section: 0)
toDos.append(toDo)
// use a function that tells me if it is tomorrow, today or yesterday
let calendar: Calendar = {
var calendar = Calendar.current
calendar.timeZone = TimeZone.current
return calendar
}()
if calendar.isDateInToday(toDo.dueDate) && optionDaySegmentedControl.selectedSegmentIndex == 1 {
tableView.insertRows(at: [newIndexPath], with: .automatic)
} else if (calendar.isDateInTomorrow(toDo.dueDate) && optionDaySegmentedControl.selectedSegmentIndex == 2) {
tableView.insertRows(at: [newIndexPath], with: .automatic)
} else if (calendar.isDateInYesterday(toDo.dueDate) && optionDaySegmentedControl.selectedSegmentIndex == 0) {
tableView.insertRows(at: [newIndexPath], with: .automatic)
}
}
toDoList.append(toDo) // update the first todo list
switch ToDoController.shared.saveToDos(toDos: toDoList) { // save the original todo list
case .success:
print("success writing data after edit complete object")
case .failureWritingData(let error):
print("failure writing data, \(error)", error.localizedDescription)
case .failureCreatingEncoder(let error):
print("failure creating encoder, \(error)", error.localizedDescription)
}
}
case PropertyKeys.CancelUnwind:
if let selectedIndexPath = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: selectedIndexPath, animated: true)
}
default:
preconditionFailure("No segue identified")
}
}
It has several issues right now, but it the target is accomplished. Create new commits.
- Update ToDo.swift
struct ToDo: Codable {
var projectName: String
var title: String
var isComplete: Bool
var dueDate: Date
var startDate: Date?
var finishDate: Date?
var note: String?
init(projectName: String, title: String, isComplete: Bool, dueDate: Date, note: String?)
{
self.projectName = projectName
self.title = title
self.isComplete = isComplete
self.dueDate = dueDate
self.note = note
}
static let dueDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .short
return formatter
}()
func categorizedDay() -> OptionDay {
let calendar: Calendar = {
var calendar = Calendar.current
calendar.timeZone = TimeZone.current
return calendar
}()
if calendar.isDateInToday(self.dueDate) {
return .today
} else if calendar.isDateInTomorrow(self.dueDate) {
return .tomorrow
} else if calendar.isDateInYesterday(self.dueDate) {
return .yesterday
} else {
return .otherTime
}
}
}
- update ToDoController.swift:
import Foundation
enum SaveToDoResult {
case success
case failureCreatingEncoder(Error)
case failureWritingData(Error)
}
enum URLError: Error {
case invalidURL
}
enum DataError: Error {
case invalidData
}
enum LoadToDoResult {
case success([ToDo])
case failureInvalidURL(URLError)
case failureInvalidData(String)
}
class ToDoController {
static let shared = ToDoController()
static let DocumentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
static let ArchiveURL = DocumentsDirectory.appendingPathComponent("todos").appendingPathExtension("plist")
func prototypeMark1LoadToDos(completion: @escaping (LoadToDoResult) -> Void) {
var codedToDos: Data
do {
codedToDos = try Data(contentsOf: ToDoController.ArchiveURL)
} catch let error {
return completion(.failureInvalidURL(error as! URLError))
}
let propertyListDecoder = PropertyListDecoder()
if let toDos = try? propertyListDecoder.decode(Array<ToDo>.self, from: codedToDos) {
return completion(.success(toDos))
} else {
return completion(.failureInvalidData("System couldn't decode"))
}
}
func prototypeMark2LoadToDos() -> [ToDo]? {
var codedToDos: Data
do {
codedToDos = try Data(contentsOf: ToDoController.ArchiveURL)
} catch {
return nil
}
do {
let propertyListDecoder = PropertyListDecoder()
let toDos = try propertyListDecoder.decode(Array<ToDo>.self, from: codedToDos)
return toDos
} catch {
return nil
}
}
func saveToDos(toDos: [ToDo]) -> SaveToDoResult {
let propertyListEncoder = PropertyListEncoder()
var codedToDos: Data
do {
codedToDos = try propertyListEncoder.encode(toDos)
} catch let error {
return .failureCreatingEncoder(error)
}
do {
try codedToDos.write(to: ToDoController.ArchiveURL, options: .noFileProtection)
return .success
} catch let error {
return .failureWritingData(error)
}
}
static func loadSampleToDos() -> [ToDo] {
let todo1 = ToDo(projectName: "para antier", title: "Para antier", isComplete: false, dueDate: Date().addingTimeInterval(-2*(24*60*60)), note: "para antier")
let todo2 = ToDo(projectName: "para ayer", title: "para ayer", isComplete: false, dueDate: Date().addingTimeInterval(-24*60*60), note: "Sample Note 2")
let todo3 = ToDo(projectName: "para hoy", title: "Para Hoy", isComplete: false, dueDate: Date(), note: "Sample Note 3")
let todo4 = ToDo(projectName: "para mañana", title: "Para mañana", isComplete: false, dueDate: Date().addingTimeInterval(24*60*60), note: "Sample Note 4")
let todo5 = ToDo(projectName: "para pasado mañana", title: "para pasado mañana", isComplete: false, dueDate: Date().addingTimeInterval(2*24*60*60), note: "Sample Note 5")
return [todo1, todo2, todo3, todo4, todo5]
}
func toDosFor(optionDay: OptionDay, toDos: [ToDo]) -> [ToDo] {
var selectedTodos = [ToDo]()
let calendar: Calendar = {
var calendar = Calendar.current
calendar.timeZone = TimeZone.current
return calendar
}()
switch optionDay {
case .today:
for toDo in toDos {
if calendar.isDateInToday(toDo.dueDate) {
selectedTodos.append(toDo)
}
}
case .tomorrow:
for toDo in toDos {
if calendar.isDateInTomorrow(toDo.dueDate) {
selectedTodos.append(toDo)
}
}
case .yesterday:
for toDo in toDos {
if calendar.isDateInYesterday(toDo.dueDate) {
selectedTodos.append(toDo)
}
}
case .otherTime:
for toDo in toDos {
if !calendar.isDateInYesterday(toDo.dueDate) && !calendar.isDateInToday(toDo.dueDate) && !calendar.isDateInTomorrow(toDo.dueDate) {
selectedTodos.append(toDo)
}
}
}
return selectedTodos
}
}
- Update ToDoViewController.swift:
enum OptionDay {
case yesterday, today, tomorrow, otherTime
}
class ToDoViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, ToDoTableViewCellDelegate {
@IBOutlet weak var tableView: UITableView!
struct PropertyKeys {
static let ToDoCellIdentifier = "ToDoCellIdentifier"
static let ShowDetailsToDo = "ShowDetailsToDo"
static let SaveUnwind = "SaveUnwind"
static let CancelUnwind = "CancelUnwind"
}
@IBOutlet weak var editBarButtonItem: UIBarButtonItem!
@IBOutlet weak var calendarView: UIView!
var optionDay: OptionDay = .today
var currentToDos = [ToDo]()
var yesterdayToDos = [ToDo]()
var todayToDos = [ToDo]()
var tomorrowToDos = [ToDo]()
var otherTimeToDos = [ToDo]()
@IBOutlet weak var optionDaySegmentedControl: UISegmentedControl!
override func viewDidLoad() {
tableView.delegate = self
tableView.register(UINib(nibName: "ToDoTableViewCell", bundle: nil), forCellReuseIdentifier: "ToDoCellIdentifier")
editBarButtonItem.title = "Edit"
loadToDos()
loadCalendarView()
}
func loadToDos() {
ToDoController.shared.prototypeMark1LoadToDos { (LoadToDoResult) in
switch LoadToDoResult {
case .success(let obteinedToDos):
self.currentToDos = obteinedToDos
self.todayToDos = ToDoController.shared.toDosFor(optionDay: .today, toDos: obteinedToDos)
self.optionDaySegmentedControl.selectedSegmentIndex = 1
case .failureInvalidData(let message):
print("\(message)")
self.currentToDos = ToDoController.loadSampleToDos()
case .failureInvalidURL(let urlError):
print("\(urlError.localizedDescription)")
self.currentToDos = ToDoController.loadSampleToDos()
}
}
}
func loadCalendarView() {
Bundle.main.loadNibNamed("CalendarView", owner: self, options: nil)
calendarView.frame.origin = CGPoint(x: 0, y: 55)
view.addSubview(calendarView)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch optionDaySegmentedControl.selectedSegmentIndex {
case 0:
return yesterdayToDos.count
case 1:
return todayToDos.count
case 2:
return tomorrowToDos.count
case 3:
return otherTimeToDos.count
default:
preconditionFailure("unrecognize case")
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: PropertyKeys.ToDoCellIdentifier) as? ToDoTableViewCell else { fatalError("Could not dequeue a cell") }
var toDo: ToDo
switch optionDaySegmentedControl.selectedSegmentIndex {
case 0:
toDo = yesterdayToDos[indexPath.row]
case 1:
toDo = todayToDos[indexPath.row]
case 2:
toDo = tomorrowToDos[indexPath.row]
case 3:
toDo = otherTimeToDos[indexPath.row]
default:
preconditionFailure("unrecognize case")
}
cell.titleTextField?.text = toDo.title
cell.isCompleteButton.isSelected = toDo.isComplete
cell.delegate = self
return cell
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let title = "Delete"
var name: String
switch optionDaySegmentedControl.selectedSegmentIndex {
case 0:
name = yesterdayToDos[indexPath.row].title
case 1:
name = todayToDos[indexPath.row].title
case 2:
name = tomorrowToDos[indexPath.row].title
case 3:
name = otherTimeToDos[indexPath.row].title
default:
preconditionFailure("unrecognize case")
}
let messageFormatted = String(format: "delete: \(name) ?", name)
let alertController = UIAlertController(title: title, message: messageFormatted, preferredStyle: .actionSheet)
let destructiveAction = UIAlertAction(title: "Delete", style: .destructive) { (_) in
switch self.optionDaySegmentedControl.selectedSegmentIndex {
case 0:
self.yesterdayToDos.remove(at: indexPath.row)
case 1:
self.todayToDos.remove(at: indexPath.row)
case 2:
self.tomorrowToDos.remove(at: indexPath.row)
case 3:
self.otherTimeToDos.remove(at: indexPath.row)
default:
preconditionFailure("unrecognize case")
}
tableView.deleteRows(at: [indexPath], with: .fade)
self.saveCurrentToDos()
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alertController.addAction(destructiveAction)
alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil)
}
}
func saveCurrentToDos() {
self.currentToDos.removeAll()
self.currentToDos = self.yesterdayToDos
self.currentToDos.append(contentsOf: self.todayToDos)
self.currentToDos.append(contentsOf: self.tomorrowToDos)
self.currentToDos.append(contentsOf: self.otherTimeToDos)
switch ToDoController.shared.saveToDos(toDos: self.currentToDos) {
case .success:
print("success writing data after editing")
case .failureWritingData(let error):
print("failure writing data, \(error)", error.localizedDescription)
case .failureCreatingEncoder(let error):
print("failure creating encoder, \(error)", error.localizedDescription)
}
}
@IBAction func unwindToToDoList(segue: UIStoryboardSegue) {
switch segue.identifier {
case PropertyKeys.SaveUnwind:
let sourceViewController = segue.source as! NewToDoViewController
if let toDo = sourceViewController.toDo {
if let selectedIndexPath = tableView.indexPathForSelectedRow {
switch self.optionDaySegmentedControl.selectedSegmentIndex {
case 0:
yesterdayToDos[selectedIndexPath.row] = toDo
case 1:
todayToDos[selectedIndexPath.row] = toDo
case 2:
tomorrowToDos[selectedIndexPath.row] = toDo
case 3:
otherTimeToDos[selectedIndexPath.row] = toDo
default:
preconditionFailure("unrecognize case")
}
tableView.reloadRows(at: [selectedIndexPath], with: .none)
tableView.deselectRow(at: selectedIndexPath, animated: true)
self.saveCurrentToDos()
} else {
var newIndexPath: IndexPath
let optionDay = toDo.categorizedDay()
switch optionDay {
case .yesterday:
newIndexPath = IndexPath(row: yesterdayToDos.count, section: 0)
yesterdayToDos.append(toDo)
case .today:
newIndexPath = IndexPath(row: todayToDos.count, section: 0)
todayToDos.append(toDo)
case .tomorrow:
newIndexPath = IndexPath(row: tomorrowToDos.count, section: 0)
tomorrowToDos.append(toDo)
case .otherTime:
newIndexPath = IndexPath(row: otherTimeToDos.count, section: 0)
otherTimeToDos.append(toDo)
}
if toDo.categorizedDay() == .yesterday && optionDaySegmentedControl.selectedSegmentIndex == 0 {
tableView.insertRows(at: [newIndexPath], with: .automatic)
} else if toDo.categorizedDay() == .today && optionDaySegmentedControl.selectedSegmentIndex == 1 {
tableView.insertRows(at: [newIndexPath], with: .automatic)
} else if toDo.categorizedDay() == .tomorrow && optionDaySegmentedControl.selectedSegmentIndex == 2 {
tableView.insertRows(at: [newIndexPath], with: .automatic)
} else if toDo.categorizedDay() == .otherTime && optionDaySegmentedControl.selectedSegmentIndex == 3 {
tableView.insertRows(at: [newIndexPath], with: .automatic)
}
}
self.saveCurrentToDos()
}
case PropertyKeys.CancelUnwind:
if let selectedIndexPath = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: selectedIndexPath, animated: true)
}
default:
preconditionFailure("No segue identified")
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == PropertyKeys.ShowDetailsToDo {
let toDoViewController = segue.destination as! NewToDoViewController
let selectedIndexPath = tableView.indexPathForSelectedRow!
var selectedToDo: ToDo
switch optionDaySegmentedControl.selectedSegmentIndex {
case 0:
selectedToDo = yesterdayToDos[selectedIndexPath.row]
case 1:
selectedToDo = todayToDos[selectedIndexPath.row]
case 2:
selectedToDo = tomorrowToDos[selectedIndexPath.row]
case 3:
selectedToDo = otherTimeToDos[selectedIndexPath.row]
default:
preconditionFailure("unricognized case")
}
toDoViewController.toDo = selectedToDo
}
}
// MARK: Conform the Protocol
func checkmarkTapped(sender: ToDoTableViewCell) {
if let indexPath = tableView.indexPath(for: sender) {
var toDo: ToDo
switch optionDaySegmentedControl.selectedSegmentIndex {
case 0:
toDo = yesterdayToDos[indexPath.row]
toDo.isComplete = !toDo.isComplete
yesterdayToDos[indexPath.row] = toDo
case 1:
toDo = todayToDos[indexPath.row]
toDo.isComplete = !toDo.isComplete
todayToDos[indexPath.row] = toDo
case 2:
toDo = tomorrowToDos[indexPath.row]
toDo.isComplete = !toDo.isComplete
tomorrowToDos[indexPath.row] = toDo
case 3:
toDo = otherTimeToDos[indexPath.row]
toDo.isComplete = !toDo.isComplete
otherTimeToDos[indexPath.row] = toDo
default:
preconditionFailure("unricognize case")
}
tableView.reloadRows(at: [indexPath], with: .automatic)
self.saveCurrentToDos()
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: PropertyKeys.ShowDetailsToDo, sender: self)
}
@IBAction func editBarButtonItemTapped(_ sender: UIBarButtonItem) {
if !tableView.isEditing {
tableView.setEditing(true, animated: true)
sender.title = "Done"
} else {
tableView.setEditing(false, animated: true)
sender.title = "Edit"
}
}
@IBAction func toDosForSelectedDay(_ sender: UISegmentedControl) {
var updatedToDos = [ToDo]()
ToDoController.shared.prototypeMark1LoadToDos { (LoadToDoResult) in
switch LoadToDoResult {
case .success(let obteinedToDos):
updatedToDos = obteinedToDos
case .failureInvalidData(let message):
print("\(message)")
self.currentToDos = ToDoController.loadSampleToDos()
case .failureInvalidURL(let urlError):
print("\(urlError.localizedDescription)")
self.currentToDos = ToDoController.loadSampleToDos()
}
}
switch sender.selectedSegmentIndex {
case 0:
self.yesterdayToDos = ToDoController.shared.toDosFor(optionDay: .yesterday, toDos: updatedToDos)
case 1:
self.todayToDos = ToDoController.shared.toDosFor(optionDay: .today, toDos: updatedToDos)
case 2:
self.tomorrowToDos = ToDoController.shared.toDosFor(optionDay: .tomorrow, toDos: updatedToDos)
case 3:
self.otherTimeToDos = ToDoController.shared.toDosFor(optionDay: .otherTime, toDos: updatedToDos)
default:
preconditionFailure("There is no other option")
}
tableView.reloadData()
}
}
This is the Ugly piece of code:
@IBAction func unwindToToDoList(segue: UIStoryboardSegue) {
switch segue.identifier {
case PropertyKeys.SaveUnwind:
let sourceViewController = segue.source as! NewToDoViewController
if let toDo = sourceViewController.toDo {
if let selectedIndexPath = tableView.indexPathForSelectedRow {
switch self.optionDaySegmentedControl.selectedSegmentIndex {
case 0:
yesterdayToDos[selectedIndexPath.row] = toDo
case 1:
todayToDos[selectedIndexPath.row] = toDo
case 2:
tomorrowToDos[selectedIndexPath.row] = toDo
case 3:
otherTimeToDos[selectedIndexPath.row] = toDo
default:
preconditionFailure("unrecognize case")
}
tableView.reloadRows(at: [selectedIndexPath], with: .none)
tableView.deselectRow(at: selectedIndexPath, animated: true)
self.saveCurrentToDos()
} else {
var newIndexPath: IndexPath
let optionDay = toDo.categorizedDay()
switch optionDay {
case .yesterday:
newIndexPath = IndexPath(row: yesterdayToDos.count, section: 0)
yesterdayToDos.append(toDo)
case .today:
newIndexPath = IndexPath(row: todayToDos.count, section: 0)
todayToDos.append(toDo)
case .tomorrow:
newIndexPath = IndexPath(row: tomorrowToDos.count, section: 0)
tomorrowToDos.append(toDo)
case .otherTime:
newIndexPath = IndexPath(row: otherTimeToDos.count, section: 0)
otherTimeToDos.append(toDo)
}
if toDo.categorizedDay() == .yesterday && optionDaySegmentedControl.selectedSegmentIndex == 0 {
tableView.insertRows(at: [newIndexPath], with: .automatic)
} else if toDo.categorizedDay() == .today && optionDaySegmentedControl.selectedSegmentIndex == 1 {
tableView.insertRows(at: [newIndexPath], with: .automatic)
} else if toDo.categorizedDay() == .tomorrow && optionDaySegmentedControl.selectedSegmentIndex == 2 {
tableView.insertRows(at: [newIndexPath], with: .automatic)
} else if toDo.categorizedDay() == .otherTime && optionDaySegmentedControl.selectedSegmentIndex == 3 {
tableView.insertRows(at: [newIndexPath], with: .automatic)
}
}
self.saveCurrentToDos()
}
case PropertyKeys.CancelUnwind:
if let selectedIndexPath = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: selectedIndexPath, animated: true)
}
default:
preconditionFailure("No segue identified")
}
}
This is the improvement:
@IBAction func unwindToToDoList(segue: UIStoryboardSegue) {
switch segue.identifier {
case PropertyKeys.SaveUnwind:
let sourceViewController = segue.source as! NewToDoViewController
if let toDo = sourceViewController.toDo {
if let selectedIndexPath = tableView.indexPathForSelectedRow {
switch self.optionDaySegmentedControl.selectedSegmentIndex {
case 0:
yesterdayToDos[selectedIndexPath.row] = toDo
case 1:
todayToDos[selectedIndexPath.row] = toDo
case 2:
tomorrowToDos[selectedIndexPath.row] = toDo
case 3:
otherTimeToDos[selectedIndexPath.row] = toDo
default:
preconditionFailure("unrecognize case")
}
tableView.reloadRows(at: [selectedIndexPath], with: .none)
tableView.deselectRow(at: selectedIndexPath, animated: true)
self.saveCurrentToDos()
} else {
var newIndexPath: IndexPath
let optionDay = toDo.categorizedDay()
switch optionDay {
case .yesterday:
newIndexPath = IndexPath(row: yesterdayToDos.count, section: 0)
yesterdayToDos.append(toDo)
if optionDaySegmentedControl.selectedSegmentIndex == 0 {
tableView.insertRows(at: [newIndexPath], with: .automatic)
}
case .today:
newIndexPath = IndexPath(row: todayToDos.count, section: 0)
todayToDos.append(toDo)
if optionDaySegmentedControl.selectedSegmentIndex == 1 {
tableView.insertRows(at: [newIndexPath], with: .automatic)
}
case .tomorrow:
newIndexPath = IndexPath(row: tomorrowToDos.count, section: 0)
tomorrowToDos.append(toDo)
if optionDaySegmentedControl.selectedSegmentIndex == 2 {
tableView.insertRows(at: [newIndexPath], with: .automatic)
}
case .otherTime:
newIndexPath = IndexPath(row: otherTimeToDos.count, section: 0)
otherTimeToDos.append(toDo)
if optionDaySegmentedControl.selectedSegmentIndex == 3 {
tableView.insertRows(at: [newIndexPath], with: .automatic)
}
}
}
self.saveCurrentToDos()
}
case PropertyKeys.CancelUnwind:
if let selectedIndexPath = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: selectedIndexPath, animated: true)
}
default:
preconditionFailure("No segue identified")
}
}
As you can see, once you get the new toDo object and append it to the correct array, you insert the row into the correct IndexPath if is selected the corresponding segment.
- Remove startButton (it is redundant to have it) from ToDoTableViewCell.xib file.
- remove constrains of each subview, (textfield and buttons).
- Construct the new constraints for checkBox button from ToDoTableViewCell.xib file and remove IBOutlet button from ToDoTableViewCell.swift file
- Find
viewDidLoad()
's NewToDoViewController method, if you are going to create a new ToDo, make titleTextField becone first responder.
override func viewDidLoad() {
super.viewDidLoad()
if let toDo = toDo { // if you inject a ToDo
navigationItem.title = "To-Do"
titleTextField.text = toDo.title
isCompletButton.isSelected = toDo.isComplete
dueDatePickerView.date = toDo.dueDate
notesTextView.text = toDo.note
} else { // if you don't inject a ToDo
dueDatePickerView.date = Date().addingTimeInterval(24*60*60)
dueDateLabel.textColor = .gray
titleTextField.becomeFirstResponder()
}
updateDueDateLabel(date: dueDatePickerView.date)
updateSaveButtonState()
}
According to Human Interface Guidelines, avoid using a segmented control in a toolbar. I will move the segmented control outside.