iOS Project Management App Tutorial – Part 1

Hello World! This is the first half of the two-part post that will combine what we’ve been learning from several different posts into a fully-functional app: Taskr. Taskr is a classic project management app where the user is trying to keep track of all of the tasks related to a particular project. While we’re building Taskr, we’ll also learn plenty of new techniques like scheduling notifications, placing badges on the app icon, and data persistence using UserDefaults.

Before reading this post, you should read through the posts on UITableView and navigation since we’ll be using those for our app. Reading the post on closures will also be useful towards the end of this first half.

Download the entire source code here.

Learn iOS by building real apps

Check out The Complete iOS Development Course – Build 14 Apps with Swift 2 on Zenva Academy to learn Swift 2 and iOS development from the ground-up with an expert trainer.

Did you come across any errors in this tutorial? Please let us know by completing this form and we’ll look into it!

FREE COURSES
Python Blog Image

FINAL DAYS: Unlock coding courses in Unity, Godot, Unreal, Python and more.

Let’s start by creating a new single-view iOS app that uses Swift. We’re going to need two views: one to display the list of tasks and another to edit selected task. Drag out a Navigation Controller in the object library pane.

Taskr - 1

This template will provide us with a navigation controller and another view controller with a UITableView already configured. Move the storyboard entry arrow so that it points to the navigation controller. Move the other view controller next to the one with the UITableView. This will be our detail view. We’ve still got some work to do with our UITableView. Select a cell and change its style to be Subtitle and its identifier to be taskCell.

Taskr - 2

We need a way to add tasks to our list. For this, we’re going to use a UIBarButtonItem. An example of one you might have seen before is the little plus icon on the top right of the navigation controller. This is exactly the same one we’re going to create by adding a navigation item from the object library into the UITableView controller. Double-click on the title and change it to Taskr. Then position a UIBarButtonItem from the object library to the right of the title.

Taskr - 3

Click on it, and, in the attributes inspector, change the System Item to be Add. This will change the icon so it looks like the add symbol.

Taskr - 4

Now that we have a way to add tasks, let’s wire up the button. Control-drag from the UIBarButtonItem to the other view controller and select the Show segue. In that new view controller, drag out a UITextField, UITextView, UIDatePicker, and UIButton and position them like the screenshot below. Add placeholder text to the UITextField, remove the lorem ipsum text from the UITextView, and change the text of the UIButton to say Save.

Taskr - 5

To quickly deal with autolayout, shift-select all of the views, and, on the very bottom right of the storyboard, open up the Resolution menu and select “Add missing constraints.”

Taskr - 6

Now that we’re finished with that, we can get to our code. First, create a new Cocoa class, call it TaskrTableViewController, and make sure it subclasses UITableViewController. In the class, replace the “Cocoa” in the import statement with “UIKit”. Rename ViewController.swift to TaskrDetailViewController.swift by slow double-clicking on the file in Xcode. Open that file and rename the ViewController class to TaskrDetailViewController. If you’re having trouble renaming, just delete the file and start fresh with the right name. Go back to the storyboard and change the class of the UITableView controller to be TaskrTableViewController. Do the same for the other view controller, except with TaskrDetailViewController as the class.

Taskr - 7

Now open up TaskrDetailViewController and create outlets for the UITextField, UITextView, and UIDatePicker. Create an action for the UIButton as well.

@IBOutlet weak var titleTextField: UITextField!
@IBOutlet weak var descriptionTextView: UITextView!
@IBOutlet weak var deadlineDatePicker: UIDatePicker!

@IBAction func save(_ sender: AnyObject) {
}

Since we’ll be dealing with notifications, similar to permissions, we have to notify the user that our app will be sending notifications. To register this service, open up the AppDelegate.swift file. Inside of application:didFinishLaunchingWithOptions method, add this one-liner.

application.registerUserNotificationSettings(UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil))

When our app first starts, this will notify the user that our app will be scheduling notifications. They can choose to accept or decline. If they choose to accept, we can schedule notifications, update the badge on the app icon, and play the notification sound.

Now that we’ve finished designing the front-end, let’s take a look at the model of our app. Create a new Swift file and call it Task. Inside it, we need a model of what a task looks like. Here’s a start:

import Foundation

struct Task {
    var ID: String
    var title: String
    var description: String
    var deadline: Date
    
}

We’re using a lightweight struct instead of a class since the purpose of this model is really just to temporarily hold data while we’re transferring it from place-to-place (i.e. saving/retrieving to/from disk to a UITableView). This is technically all we need, but we can add a few more constructs for convenience. Let’s add an initializer and a variable to store if we’ve past the deadline.

struct Task {
    var ID: String
    var title: String
    var description: String
    var deadline: Date
    
    var pastDeadline: Bool {
        return Date().compare(self.deadline) == ComparisonResult.orderedDescending
    }
    
    init(ID: String, title: String, description: String, deadline: Date) {
        self.ID = ID
        self.title = title
        self.description = description
        self.deadline = deadline
    }
    
}

The new pastDeadline property is called a computed property since its value is determined by some calculation that depends on other properties or methods. In our case, we’re simply checking if the deadline comes before, or is earlier than, today’s date.

Now that we have a representation of a single item, we need to model the entire list of items. However, we don’t want multiple different copies of this list floating around so we need to use the Singleton design pattern to make sure there’s only one static copy of the task list. The Singleton design pattern forces only one instance of a particular class to exist at a time. This is perfect for our task list!

Since Swift 1.2, creating Singletons takes a single line of code! Let’s create a new Swift file called TaskList and add the following line to make it into a Singleton.

class TaskList {
    static let sharedInstance = TaskList()
    private init() { }
}

We need to add functionality to add and remove items. As for data persistence, we can use the UserDefaults class to very simply persist our data. No need for anything as complicated as Core Data! We also need to create a unique key to store and retrieve these stored values. While we’re at it, we should also schedule a notification as soon as we add an item to the list. In the TaskList class, add the following constant and function.

private let TASKS_KEY = "tasks"

func addTask(_ task: Task) {
    var taskDictionary = UserDefaults.standard.dictionary(forKey: TASKS_KEY) ?? [:]
    
    taskDictionary[task.ID] = ["ID" : task.ID, "title" : task.title, "description" : task.description, "deadline" : task.deadline]
    UserDefaults.standard.set(taskDictionary, forKey: TASKS_KEY)
    
    let notification = UILocalNotification()
    notification.alertBody = "Task "(task.title)" Is Overdue"
    notification.alertAction = "open"
    notification.fireDate = task.deadline as Date
    notification.soundName = UILocalNotificationDefaultSoundName
    notification.userInfo = ["ID": task.ID]
    notification.category = "TASK_CATEGORY"
    UIApplication.shared.scheduleLocalNotification(notification)
}

The above code snippet uses UserDefaults to retrieve data, or an empty dictionary if none exists; then we store the new task to the dictionary and store that dictionary back into UserDefaults. Notice that we’re nesting our dictionaries. We have a top-level dictionary that maps task IDs to another dictionary that holds all of the task data. Notice that we use the nil coalescing operator (??) to return an empty dictionary in case we retrieve a nil dictionary (i.e. the first time we open up the app).

Now we can create a notification and schedule it by setting some properties and using scheduleLocalNotification. We need a way of referring to a particular notification in case the task is completed later. To do this, we can use the userInfo property on the notification to be a dictionary that stores the ID of the task that corresponds to this scheduled notification.

Since we’re dealing with a UITableView, we’ll need a method to retrieve all of the tasks in the list. We’re even going to sort them chronologically for UITableView to display. This below code snippet looks very complicated, but it’s not so bad if we break it down. The first line will extract the dictionary from disk. Then we create an array from the values, which are the tasks themselves. We use the map function to execute code in that closure to each element in the items array. In our case, we’re creating an array of all of the tasks using the initializer. Notice we use the shorthand parameter argument ($0) to reference the dictionary of data values for each task. Finally, we sort the array chronologically.

func allTasks() -> [Task] {
    let taskDictionary = UserDefaults.standard.dictionary(forKey: TASKS_KEY) as? [String : [String: Any]] ?? [:]
    let items = Array(taskDictionary.values)
    let taskArray = items.map({Task(ID: $0["ID"] as! String!,
        title: $0["title"] as! String,
        description: $0["description"] as! String,
        deadline: $0["deadline"] as! Date)})
    
    return taskArray.sorted(by: {(left: Task, right:Task) -> Bool in
        left.deadline.compare(right.deadline) == .orderedAscending})
}

Since these are tasks, we need a way to complete these tasks. When we add tasks, we add them to the master dictionary and schedule a notification. To reverse this, we need to delete the task from the dictionary and cancel the scheduled notification. In the below code snippet, we remove the task whose ID is task.ID. Then we iterate through all of the pending notifications and cancel the one whose’s userInfo dictionary has task.ID in it. Remember that we set that when we added the task.

func removeTask(_ task: Task) {
    if var taskItems = UserDefaults.standard.dictionary(forKey: TASKS_KEY) {
        taskItems.removeValue(forKey: task.ID)
        UserDefaults.standard.set(taskItems, forKey: TASKS_KEY)
    }
    
    for notification in UIApplication.shared.scheduledLocalNotifications! {
        if notification.userInfo!["ID"] as! String == task.ID {
            UIApplication.shared.cancelLocalNotification(notification)
        }
    }
}

That’s all we’ll do in this post, but in the next post, we’ll see how we can handle notifications and wire up our model to our view using our two controllers. In this post, we built the view and model of our Taskr app. We learned about some new functions, like map and sort, as well as notifications and data persistence using UserDefaults.