8 - To Do App Part 2

Welcome back. In the previous tutorial we did quite a bit with our “Do It Up” application. In this tutorial we will continue where we left off. We are going to be saving data from the form, edit existing data, deleting data, adding images, and sorting data.

Quick updates

There are a few bugs that I noticed during development that we will fix now (or later)

When running the app on your mac’s iphone simulator, we do not get the same sense of the keyboard, so we still have the issue where if the keyboard pops up, it doesn’t go down on return or when touching another portion of the screen. I added the following code (the same from CrazyLib) to fix that (in TaskDetailVC.swift):

func hideKeyboardWhenTappedAround() {
    let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(TaskDetailVC.dismissKeyboard))
    view.addGestureRecognizer(tap)
}

func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    dismissKeyboard();
    return false
}

func dismissKeyboard() {
    view.endEditing(true)
}

and then the following inside of viewDidLoad():

titleField.delegate = self
detailField.delegate = self
hideKeyboardWhenTappedAround()

The second bug that I noticed is that we are currently loading the categories and priorities multiple times when loading the TaskDetailVC multiple times. We want the short data to only load once, save it to our context, and not load it again unless the user uninstalls and reinstalls the app. To fix this I have rearranged and added a couple conditionals in our TaskDetailVC.swift in the viewDidLoad():

override func viewDidLoad() {
    super.viewDidLoad()
    
    // Do any additional setup after loading the view.
    titleField.delegate = self
    detailField.delegate = self
    
    categoryPickerView.isHidden = true
    priorityPickerView.isHidden = true
    datePicker.isHidden = true
    selectButton.isHidden = true
    categoryPickerView.delegate = self
    priorityPickerView.delegate = self
    categoryPickerView.dataSource = self
    priorityPickerView.dataSource = self
    
    getCategories()
    getPriorities()
    
    if categoryPickerView.numberOfRows(inComponent: 0) == 0 {
        let category1 = Category(context: context)
        category1.category = "Work"
        let category2 = Category(context: context)
        category2.category = "School"
        let category3 = Category(context: context)
        category3.category = "Hobby"
        let category4 = Category(context: context)
        category4.category = "Family"
        let category5 = Category(context: context)
        category5.category = "Tech"
        let category6 = Category(context: context)
        category6.category = "Other"
        ad.saveContext()
        getCategories()
    }
    if priorityPickerView.numberOfRows(inComponent: 0) == 0 {
        let priority1 = Priority(context: context)
        priority1.priority = "High"
        let priority2 = Priority(context: context)
        priority2.priority = "Medium"
        let priority3 = Priority(context: context)
        priority3.priority = "Low"
        let priority4 = Priority(context: context)
        priority4.priority = "None"
        ad.saveContext()
        getPriorities()
    }
    
    hideKeyboardWhenTappedAround()
}

This way if the context has no elements within it, it loads our pre-made data and it will not again because our pickerViews will be populated next time.

Ok now let’s continue.

Saving Data

So in our Add/Edit form, we need to save the task that we create through the form textfields and selectors in our scene. They way we can do that is to first make an IBAction out of our save button. Control-Drag from the button to our TaskDetailVC to create the action:

@IBAction func savePressed(_ sender: Any) {
    // put what we want to happen in here
}

Now we have the following items to set for our new task:

  1. created (Date)
  2. details (String)
  3. title (String)
  4. due (Date)
  5. toCategory (Category)
  6. toImage (Image)
  7. toPriority (Priority)

To start, we will do everything but image:

@IBAction func savePressed(_ sender: Any) {
    // create our task
    let task = Task(context: context)
    // set the title
    if let title = titleField.text {
        task.title = title
    }
    // set the detail
    if let detail = detailField.text {
        task.details = detail
    }
    // set the created date
    task.created = NSDate()
    // take our date
    task.due = datePicker.date as NSDate
    // take our category and priority
    task.toCategory = categories[categoryPickerView.selectedRow(inComponent: 0)]
    task.toPriority = priorities[priorityPickerView.selectedRow(inComponent: 0)]
    // save our item 
    ad.saveContext()
    // go back to the main page when we hit save
    _ = navigationController?.popViewController(animated: true)
    
}

Once we have done this we can do the following: saved

Editing Existing Data

Our goal for editing existing data is being able to click on a task, open up the data in our add/edit pane, and then save the task.

Create a variable in TaskDetailVC.swift to store our task that we want to edit:

var taskEdit: Task?

In the view did load, call a function if our task is not nil (we are editing a task). The function will open up our data:

if taskEdit != nil {
    loadTaskData()
}
func loadTaskData() {
    if let task = taskEdit {
        titleField.text = task.title
        detailField.text = task.details
        
        if let category = task.toCategory {
            
            var index = 0
            for cat in categories {
                if category.category == cat.category {
                    categoryPickerView.selectRow(index, inComponent: 0, animated: false)
                    pickCategoryButton.setTitle(category.category, for: .normal)
                    break
                }
                index += 1
            }
        }
        
        if let priority = task.toPriority {
            var index = 0
            for prio in priorities {
                if priority.priority == prio.priority {
                    priorityPickerView.selectRow(index, inComponent: 0, animated: false)
                    pickPriorityButton.setTitle(priority.priority, for: .normal)
                    break
                }
                index += 1
            }
        }
        if task.due != nil {
            datePicker.date = task.due! as Date
            pickDueDateButton.setTitle(task.due?.description, for: .normal)
        }
        
    }
}

Update our savePressed to set our current task so that we do not create duplicated:

    @IBAction func savePressed(_ sender: Any) {
        // This top part is the only thing that has changed
        var task: Task!
        if taskEdit == nil {
            task = Task(context: context)
        } else {
            task = taskEdit
        }
        
        
        if let title = titleField.text {
            task.title = title
        }
        if let detail = detailField.text {
            task.details = detail
        }
        task.created = NSDate()
        task.due = datePicker.date as NSDate
        
        task.toCategory = categories[categoryPickerView.selectedRow(inComponent: 0)]
        task.toPriority = priorities[priorityPickerView.selectedRow(inComponent: 0)]
        
        ad.saveContext()
        
        _ = navigationController?.popViewController(animated: true)
    }

In our ViewController.swift, we need to pass the selected task through the seque to the Add/Edit scene. Make sure your identifier for your seque is “UpdateTask”. This is for the seque from the view controller itself, not the plus button.

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    
    if let objs = fetchedResultsController.fetchedObjects, objs.count > 0{
        let task = objs[indexPath.row]
        performSegue(withIdentifier: "UpdateTask", sender: task)
    }
}

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "UpdateTask" {
        if let destination = segue.destination as? TaskDetailVC {
            if let task = sender as? Task {
                destination.taskEdit = task
            }
        }
    }
}

update

Deleting Tasks

Very easy to do. Simply make an IBAction with the trashcan bar button in our TaskDetail Scene. Here is the function body:

@IBAction func deletePressed(_ sender: Any) {
    if taskEdit != nil {
        context.delete(taskEdit!)
        ad.saveContext()
        _ = navigationController?.popViewController(animated: true)
    }
}

You should be able to successfully delete now: delete

Adding Images

Incoperate the following Delegates in our TaskDetailsVC class:

// the new ones of UIImagePickerControllerDelegate and UINavigationControllerDelegate
class TaskDetailVC: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource, UITextFieldDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate {

Create an image picker:

var imagePicker: UIImagePickerController!

In our viewdidload():

imagePicker = UIImagePickerController()
imagePicker.delegate = self

Go to info.plist. This is the file in which we are going to give information to present the user with a warning letting them know that our app is going to access their photo library:

info

Create a new function at the bottom of our class:

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
    if let image = info[UIImagePickerControllerOriginalImage] as? UIImage{
        taskImage.image = image
    }
    imagePicker.dismiss(animated: true, completion: nil)
}

Next we need to make our IBAction with our button thats hidden behind our image:

 @IBAction func addImagePressed(_ sender: Any) {
    present(imagePicker, animated: true, completion: nil)
}

Time to add it to our core data so that it remembers:

// in our savePressed():
...
let image = Image(context: context)
image.image = taskImage.image
task.toImage = image
...
// in our loadTaskData:
...
taskImage.image = task.toImage?.image as? UIImage
...
// in our TaskCell.swift within configureCell:
...
categoryLabel.text = task.toCategory?.category
priorityLabel.text = task.toPriority?.priority
taskImage.image = task.toImage?.image as? UIImage
...

Pretty neat, huh?

images

Sorting

The last piece of the puzzle is to figure out our sorting segments. We have 4 so far and we have been organizing them based on recent. Time to do some other sorting schemes. We have edited our attemptFetch() as well as placed an IBAction for our segment controller:

@IBAction func segmentChange(_ sender: Any) {   
    attemptFetch()
    tableView.reloadData()
}


func attemptFetch() {
    // This is our request for getting our Tasks
    let request: NSFetchRequest<Task> = Task.fetchRequest()
    
    // This is one of our sorting mechanisms, by date using the created portion of our Task model
    let dateSort = NSSortDescriptor(key: "created", ascending: false)
    let titleSort = NSSortDescriptor(key: "title", ascending: true)
    let dueSort = NSSortDescriptor(key: "due", ascending: true)

    // this currently doesn't work correctly, no comparator usage for core data right now :(
    let prioritySort = NSSortDescriptor(key: "toPriority.priority", ascending: true)
    
    if segmentController.selectedSegmentIndex == 0 {
        // setting our request to sort based on our date sort descriptor
        request.sortDescriptors = [dateSort]
    } else if segmentController.selectedSegmentIndex == 1 {
        request.sortDescriptors = [titleSort]
    } else if segmentController.selectedSegmentIndex == 2 {
        request.sortDescriptors = [prioritySort]
    } else if segmentController.selectedSegmentIndex == 3 {
        request.sortDescriptors = [dueSort]
    }
    

    // we create a controller to house our request that can actually perform the request
    // we also pass in our context that we made accessible in our AppDelagate.swift
    let controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
    controller.delegate = self
    
    self.fetchedResultsController = controller
    
    do {
        // we try to perform a fetch
        try controller.performFetch()
    } catch {
        // if there is an error then we print it out
        let error = error as NSError
        print("\(error)")
    }
}

And here we go:

alldone

There are a ton of different functionalities that we can add to our app. We could make another page to edit the categories that we can choose from, or even priorities. We can incorperate actual time stamps, alarms, Push notifications, etc. I’ll leave that up to you. Here’s something extra that doesn’t take too much work ;)

extra

Tags:

Updated: