Skip to main content

3 - Showing the List of Tasks

Almost done! We have our UI in place and Ditto installed, so let's add the logic to create and display tasks by using Ditto's APIs.

3-1 Create Your Ditto App on the Portal

Before we start coding, we first need to create a new app in the portal. Apps created on the portal will automatically sync data between them and also to the Ditto Big Peer.

Each app created on the portal has a unique appID which can be seen on your app's settings page once the app has been created. This ID is used in subsequent sections to configure your Ditto instance.

3-2 Setup TasksTableViewController

First, we need to add some variables that will be created on viewDidLoad of the TasksTableViewController so adjust the class to match this code:

// Remember to import DittoSwift!import DittoSwift
class TaskTableViewController: UITableViewController {    // These hold references to Ditto for easy access    var ditto: Ditto!    var store: DittoStore!    var liveQuery: DittoLiveQuery?    var subscription: DittoSubscription?    var collection: DittoCollection!
    // This is the UITableView data source    var tasks: [DittoDocument] = []
    override func viewDidLoad() {        super.viewDidLoad()                // Create an instance of Ditto        ditto = Ditto(identity: .onlinePlayground(appID: "YOUR_APP_ID_HERE", token: "YOUR_TOKEN_HERE"))
        // This starts Ditto's background synchronization        ditto.startSync()
        // Create some helper variables for easy access        store = ditto.store        // We will store data in the "tasks" collection        // Ditto stores data as collections of documents        collection = store.collection("tasks")
        // Subscribe to changes with a live-query         subscription = collection.find("!isDeleted").subscribe()
        // This function will create a "live-query" that will update        // our UITableView        setupTaskList()    }
    func setupTaskList() {        // Query for all tasks        // Observe changes and update the UITableView when anything changes        liveQuery = collection.find("!isDeleted").observeLocal { [weak self] docs, event in            guard let `self` = self else { return }            switch event {            case .update(let changes):                guard changes.insertions.count > 0 || changes.deletions.count > 0 || changes.updates.count > 0  || changes.moves.count > 0 else { return }                DispatchQueue.main.async {                    self.tableView.beginUpdates()                    self.tableView.performBatchUpdates({                        let deletionIndexPaths = changes.deletions.map { idx -> IndexPath in                            return IndexPath(row: idx, section: 0)                        }                        self.tableView.deleteRows(at: deletionIndexPaths, with: .automatic)                        let insertionIndexPaths = changes.insertions.map { idx -> IndexPath in                            return IndexPath(row: idx, section: 0)                        }                        self.tableView.insertRows(at: insertionIndexPaths, with: .automatic)                        let updateIndexPaths = changes.updates.map { idx -> IndexPath in                            return IndexPath(row: idx, section: 0)                        }                        self.tableView.reloadRows(at: updateIndexPaths, with: .automatic)                        for move in changes.moves {                            let from = IndexPath(row: move.from, section: 0)                            let to = IndexPath(row: move.to, section: 0)                            self.tableView.moveRow(at: from, to: to)                        }                    }) { _ in }                    // Set the tasks array backing the UITableView to the new documents                    self.tasks = docs                    self.tableView.endUpdates()                }            case .initial:                // Set the tasks array backing the UITableView to the new documents                self.tasks = docs                DispatchQueue.main.async {                    self.tableView.reloadData()                }            default: break            }        }    }
// remaining TaskTableViewController code...
}

Let's breakdown what this code does. First, we create the variables needed and then initialize them in viewDidLoad() . To enable background synchronization, we need to call startSync() which allows you to control when synchronization occurs. For this application we want it to run the entire time the app is in use.

// These hold references to Ditto for easy accessvar ditto: Ditto!var store: DittoStore!var liveQuery: DittoLiveQuery?var collection: DittoCollection!var subscription: DittoSubscription?
// This is the UITableView data sourcevar tasks: [DittoDocument] = []
override func viewDidLoad() {    super.viewDidLoad()
    // Create an instance of Ditto    ditto = Ditto(identity: .onlinePlayground(appID: "YOUR_APP_ID_HERE", token: "YOUR_TOKEN_HERE"))
    // This starts Ditto's background synchronization    ditto.startSync()
    // Create some helper variables for easy access    store = ditto.store    // We will store data in the "tasks" collection    // Ditto stores data as collections of documents    collection = store.collection("tasks")
    // Subscribe to changes with a live-query     subscription = collection.find("!isDeleted").subscribe()

    // This function will create a "live-query" that will update    // our UITableView    setupTaskList()}

After setting up the variables and starting Ditto, we then use Ditto's key API to observe changes to the database by creating a live-query in the setupTaskList() function. This allows us to set the initial state of the UITableView after the query is immediately run and then subsequently get callbacks for any new data changes that occur locally or that were synced from other devices:

Note, that we are using the observe API in Ditto. This API performs two functions. First, it sets up a local observer for data changes in the database that match the query and second it creates a subscription for the same query that will be used to request this data from other devices. For simplicity, we are using this combined API, but you can also call them independently. To learn more, see the Observing Changes section in the documentation.

func setupTaskList() {    liveQuery = collection.find("!isDeleted").observeLocal { [weak self] docs, event in        guard let `self` = self else { return }        switch event {        case .update(let changes):            guard changes.insertions.count > 0 || changes.deletions.count > 0 || changes.updates.count > 0  || changes.moves.count > 0 else { return }            DispatchQueue.main.async {                self.tableView.beginUpdates()                self.tableView.performBatchUpdates({                    let deletionIndexPaths = changes.deletions.map { idx -> IndexPath in                        return IndexPath(row: idx, section: 0)                    }                    self.tableView.deleteRows(at: deletionIndexPaths, with: .automatic)                    let insertionIndexPaths = changes.insertions.map { idx -> IndexPath in                        return IndexPath(row: idx, section: 0)                    }                    self.tableView.insertRows(at: insertionIndexPaths, with: .automatic)                    let updateIndexPaths = changes.updates.map { idx -> IndexPath in                        return IndexPath(row: idx, section: 0)                    }                    self.tableView.reloadRows(at: updateIndexPaths, with: .automatic)                    for move in changes.moves {                        let from = IndexPath(row: move.from, section: 0)                        let to = IndexPath(row: move.to, section: 0)                        self.tableView.moveRow(at: from, to: to)                    }                }) { _ in }                // Set the tasks array backing the UITableView to the new documents                self.tasks = docs                self.tableView.endUpdates()            }        case .initial:            // Set the tasks array backing the UITableView to the new documents            self.tasks = docs            DispatchQueue.main.async {                self.tableView.reloadData()            }        default: break        }    }        collection.find("isDeleted == true").evict()}

This is a best-practice when using Ditto, since it allows your UI to simply react to data changes which can come at any time given the ad-hoc nature of how Ditto synchronizes with nearby devices. With this in place, we can now add user actions and configure the UITableview to display the tasks.

3-3 Add A Task

To allow the user to create a task we want to display an alert view in response to clicking the add bar item. Add the following code to the didClickAddTask() function we added earlier: code-tabs

@IBAction func didClickAddTask(_ sender: UIBarButtonItem) {    // Create an alert    let alert = UIAlertController(        title: "Add New Task",        message: nil,        preferredStyle: .alert)
    // Add a text field to the alert for the new task text    alert.addTextField(configurationHandler: nil)
    alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
    // Add a "OK" button to the alert.    alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak self] (_) in        guard let self = self else { return }        if let body = alert.textFields?[0].text        {            // Insert the data into Ditto            _ = try! self.collection.upsert([                "body": body,                "isCompleted": false            ])        }    }))
    // Present the alert to the user    present(alert, animated: true, completion: nil)}

Take note that this logic is using the Ditto insert() API to create a task document. Ditto's API is designed around JSON-compatible documents which are organized into collections: code-tabs

_ = try! self.collection.upsert([    "body": body,    "isCompleted": false])

3-4 Configure UITableView To Display Task List

To ensure the UITableView can display the tasks, we need to configure it. Adjust your TasksTableViewController to include the following code (these functions were already created when the file was generated by Xcode): code-tabs

// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {    return 1}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {    return tasks.count}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {    let cell = tableView.dequeueReusableCell(withIdentifier: "taskCell", for: indexPath)
    // Configure the cell...    let task = tasks[indexPath.row]    cell.textLabel?.text = task["body"].stringValue    let taskComplete = task["isCompleted"].boolValue    if taskComplete {        cell.accessoryType = .checkmark    }    else {        cell.accessoryType = .none    }
    return cell}

Earlier, we created the tasks array which is the data source to the UITableView. This code configures the UITableView to use this array and then configures the table view cell to display the task text and a checkmark on whether it is complete or not.

New and Improved Docs

Ditto has a new documentation site at https://docs.ditto.live. This legacy site is preserved for historical reference, but its content is not guaranteed to be up to date or accurate.