iOS


Create Xcode Project

The first step is to create a basic Xcode project. Go to File New → Project and select "App":

Create Xcode Project 1

Next, fill out the options with the product name: "ToDo" and choose Storyboard for the interface selection:

Create Xcode Project 2

Great! At this point, you have the basic project created. We now need to customize the project to create the specific UI for the ToDo App.


Create UI Interfaces

The UI for the app will look like this:

Create UI Interfaces 1

This requires that we create several elements:

  1. UINavigationController to represent the top navigation bar
  2. UITableViewController which contains the UITableView to display the list of tasks
  3. UIBarButtonItem which is in the navigation bar to display the UIAlertController to input the task name

For simplicity we will use the built-in storyboard to create these.

First, we need to add a new file to the project for the UITableViewController. Go to File NewFile and select "Cocoa Touch Class":

Create UI Interfaces 2

Name your class TasksTableViewController and make sure it is a subclass of UITableViewController:

Create UI Interfaces 3

You can then delete the ViewController.swift file from the project so your structure should now look like this:

Create UI Interfaces 4

Now the project is setup, so we can start to configure Main.storyboard to create the UI.


Configure Storyboard

Click on Main.storyboard to load the storyboard editor. The initial storyboard includes a scene tied to the ViewController.swift , that we deleted, so we need to remove it and add a UINavigationController . Click on the View Controller Scene on the left-hand panel and click delete so the storyboard is now empty:

Configure Storyboard 1

Now we need to add a UINavigationController into the storyboard. Click the circle with a square in it icon in the top right to display your list of UI elements and search for UINavigationController and click to add it to the storyboard. You should end up with this:

Configure Storyboard 2

This created a UINavigationController and a root view controller based on UITableViewController, but it needs to be configured to work with our TasksTableViewController.swift file. Click on the "Root View Controller Scene" and navigate the right hand menu to declare the UITableViewController as a custom class of TasksTableViewController:

Configure Storyboard 3

Next, we need to ensure that the "Navigation Controller Scene" is the initial view controller for our app, so click on it and navigate the right hand menu to set this property:

Configure Storyboard 4

Now we need to customize the UITableViewController to include the add task button. Click on the "Root View Controller Scene" and then click the "Circle with square" icon in the top right to add a UIBarButtonItem , search for "Bar Button Item" and drag it onto the top right corner of the UINavigationBar. Finally, configure the bar item to use the "System Item" "Add" in the right-hand menu:

Configure Storyboard 5

The last configuration is to adjust the name in the navigation bar to "ToDo", click on the "Root View Controller" text and navigate the right-hand menu to adjust the name:

Configure Storyboard 6

Great! We have the UI elements in place, but now we need to wire them up, or connect them with our implementation code in TasksTableViewController.


Wire Up UI

First, we need to declare a function that will called when the user clicks on the add icon in the navigation bar. To do so, click the "Show Assistant Editor" button in the top right so that our storyboard and TaskTableViewController.swift file are both displayed:

Wire up UI 1

Now, right click on the add button and drag it into the class implementation of TasksTableViewController just above the numberOfSections() function:

Wire up UI 2

Name the function didClickAddTask and adjust the type to UIBarButtonItem and click connect. You should now have a function that is wired up to be called whenever that button is clicked:

Wire up UI 3

Finally, we will need to configure UITableView in TasksTableViewController and as part of this, we need to provide a "Table view cell reuse identifier", so let's configure our storyboard cell to include an identifier. Click on the "Prototype Cells" area in your view and navigate the right-hand menu to display the configuration for the "Table View Cell" and insert taskCell as the identifier:

Wire up UI 4

Almost done! Our UI is now setup and configured, all that is left is to add Ditto and the associated logic to create and display tasks!


Install Ditto

Now, the fun part - using Ditto! We will use CocoaPods to install the SDK.


Setup CocoaPods

If you need to install CocoaPods, please follow the installation instructions at CocoaPods.org .


Integrate Ditto

Close the Todo.xcodeproj project for now because CocoaPods will create a ToDo.xcworkspace which automatically integrates the libraries into your project. Then in your terminal navigate to the directory which contains your ToDo.xcodeproj file and run:

   pod init

This will create a Podfile which you will open and add Ditto to it:

   # Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'ToDo' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for Todo
  pod 'DittoSwift', '~> 1.0.0'

end

Save the Podfile and close it then run:

   pod install --repo-update

This will install Ditto as a dependency and create a Todo.xcworkspace file with the SDK integrated. Once it is finished open up ToDo.xcworkspace. You now have Ditto installed in the application! To verify and ensure we can use the SDK add the following line to your TasksTableViewController underneath import UIKit:

   import DittoSwift

Permissions

Since iOS 13 and Xcode 11 an app must ask the user's permission to use Bluetooth. Ditto will activate Bluetooth by default, which means the user will receive a permission prompt automatically. In addition, since iOS 14 an app must ask the user's permission to use the Local Area Network to discover devices.

You must include several keys in the Info.plist file your app

  • Privacy - Local Network Usage Description
  • Privacy - Bluetooth Peripheral Usage Description
  • Privacy - Bluetooth Always Usage Description
  • A Bonjour service _http-alt._tcp.

These can be configured through Xcode's Info project settings.

Alternatively, add the keys directly to Info.plist. Right click on the Info.plist and hover to Open as and then click Source Code

<key>NSBluetoothAlwaysUsageDescription</key>
<string>Uses Bluetooth to connect and sync with nearby devices</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>Uses Bluetooth to connect and sync with nearby devices</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Uses WiFi to connect and sync with nearby devices</string>
<key>NSBonjourServices</key>
<array>
  <string>_http-alt._tcp.</string>
</array>


Create and Display 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.

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 collection: DittoCollection!

    // We need to format the task creation date into a UTC string
    var dateFormatter = ISO8601DateFormatter()

    // This is the UITableView data source
    var tasks: [DittoDocument] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        // Create an instance of Ditto
        ditto = Ditto()

        // Set your Ditto access license
        // The SDK will not work without this!
        ditto.setAccessLicense("<INSERT ACCESS LICENSE>")

        // 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")

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

    func setupTaskList() {
        // Query for all tasks and sort by dateCreated
        // Observe changes with a live-query and update the UITableView
        liveQuery = collection.findAll().sort("dateCreated", direction: .ascending).observe { [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() . The important things to note is that you need an access license to use Ditto. If you do not have one yet, reach out and we can supply one. 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 access
var ditto: Ditto!
var store: DittoStore!
var liveQuery: DittoLiveQuery?
var collection: DittoCollection!

// We need to format the task creation date into a UTC string
var dateFormatter = ISO8601DateFormatter()

// This is the UITableView data source
var tasks: [DittoDocument] = []

override func viewDidLoad() {
    super.viewDidLoad()

    // Create an instance of Ditto
    ditto = Ditto()

    // Set your Ditto access license
    // The SDK will not work without this!
    ditto.setAccessLicense("<INSERT ACCESS LICENSE>")

    // 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")

    // 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.findAll().sort("dateCreated", direction: .ascending).observe { [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
        }
    }
}

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.


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:

@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. The handler calls addNewToDoItem()
    alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak self] (_) in
        guard let self = self else { return }
        if let text = alert.textFields?[0].text
        {
            let dateString = self.dateFormatter.string(from: Date())
            // Insert the data into Ditto
            _ = try! self.collection.insert([
                "text": text,
                "dateCreated": dateString,
                "isComplete": 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:

_ = try! self.collection.insert([
    "text": text,
    "dateCreated": dateString,
    "isComplete": false
])

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):

// 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["text"].stringValue
    let taskComplete = task["isComplete"].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.


Select Task To Complete

When the user selects the task in the table view, we want to mark the task completed. Adjust your TasksTableViewController to include the following code (these functions were already created when the file was generated by Xcode):

 override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    // Deselect the row so it is not highlighted
    tableView.deselectRow(at: indexPath, animated: true)
    // Retrieve the task at the row selected
    let task = tasks[indexPath.row]
    // Update the task to mark completed
    collection.findByID(task.id).update({ (newTask) in
        newTask?["isComplete"].set(!task["isComplete"].boolValue)
    })
}

override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
    // Return false if you do not want the specified item to be editable.
    return true
}

This action makes use of Ditto's update() API where we are able to find the existing task and set the isComplete value to the opposite of its current value.


Swipe To Delete Task

Finally, we want to allow the user to delete a task by swiping the row in the table view. Adjust your TasksTableViewController to include the following code (this function was already created when the file was generated by Xcode):

// Override to support editing the table view.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
    if editingStyle == .delete {
        // Retrieve the task at the row swiped
        let task = tasks[indexPath.row]
        // Delete the task from Ditto
        collection.findByID(task.id).remove()
    }
}

This action makes use of Ditto's remove() API which will delete it.


Build and Run!

🎉 You now have a fully functioning ToDo app. Build and run it on the simulator or devices and observe the automatic data sync provided by Ditto:

iOS ToDo App Syncing
Top