A nice application of computed properties in Swift.

Recently, I was working with Swift for a university assignment, and I was faced with a small problem. A problem that I was able to solve using the power of the stored properties in Swift.

Problem

My task was to develop a simple to-do list app.

When I finished my Swift App, I wasn’t satisfied with the way I was handling the tasks. It was ugly and not that flexible.

Each task, had a due and defer date associated with them. Each task can be defer up to a specific date, and therefore the app shouldn’t show any tasks that their defer date is in the future.

My initial implementation was to have two lists of tasks, the one that has all the tasks, regardless if the defer date is in the future or not, called tasks and a filteredTasks that I was manually maintained by adding and removing tasks based on the defer constraint.

Old Implementation

I had two class attributes both of them a list of Task objects. (Task struct definition is not important here, so think of it as a simple struct storing task information like title, due date and defer date of the task):

1
2
var tasks = [Task]()
var filteredTasks = [Task]()

Whenever I had to refresh the TableView I had to remove everything from the filteredTasks and repopulate the filteredTasks with the tasks from the tasks list, like so:

func refreshTable()
{
    filteredTasks.removeAll()
    filteredTasks = self.tasks.filter({ $0.deferBy == nil || $0.deferBy! <= Date() })
}

When I had to add new tasks, I needed to add the tasks in the tasks as well as in the filteredTasks (if and only if the task to be added didn’t have a defer date).

tasks.append(task)

if  task.deferBy == nil 
{
    let newIndexPath = IndexPath(row: filteredTasks.count, section: 0)
    filteredTasks.append(task)
    tableView.insertRows(at: [newIndexPath], with: .bottom)
}

And now, when the user wants to delete a task, I had two lists to maintain. So I had to remove the task from two lists as well as from the TableView.

filteredTasks.remove(at: indexPath.row)
tasks.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)

This implementation works just fine, but is ugly, not professional and not really flexible. You have to maintain two lists when you add, remove or refresh the tasks and if I wanted to extend any new functionality probably I would have the same problem over and over.

A better solution

I was trying to find a better way to solve the problem. The solution is Computed Properties.

I refactored the code, I removed any code that maintained the filteredTasks (adding, removing and updating) and I left only the tasks list to maintain. The new filteredTasks property looks like this:

/// Computed Property that returns filtered tasks.
///
/// Some tasks are not yet ready to be shown, that happens when a defer date
/// is set by the user. The task should only be shown if the defer date is
/// in at the moment or in the past, but not in the future.
var filteredTasks : [Task]
{
    // Get only tasks that are not defered or defered is in the past.
    return self.tasks.filter({
            !$0.isDeferSet() || $0.deferBy! <= Date()
        }
    )
}

Now, whenever I have to access the filtered tasks, the filtering is done on the fly without having to maintain the filteredTasks list myself. I just now have to maintain the initial tasks list and let the computed property to return the filtered tasks for me.

© 2019 Rafael Papallas
Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.