This walk-through is a sequel to Apple’s well-known iOS programming introduction, Start Developing iOS Apps (Swift). Apple’s introduction walks us through the process of building the UI, data, and logic of an example food tracker app, culminating with a section on data persistence: storing the app data as files in the iOS device.

This series picks up where that document leaves off: syncing data between devices, through the cloud, with an offline-first design. You will achieve this using open source tools and the IBM Cloudant service.

This document is the first in the series, showing you how to use the Cloudant Sync datastore, CDTDatastore, for FoodTracker on the iOS device. Subsequent posts will cover syncing to the cloud and other advanced features such as accounts and data management.

Table of Contents

  1. Getting Started
  2. CocoaPods
    1. Learning Objectives
    2. Install CocoaPods on your Mac
    3. Install Cloudant Sync using CocoaPods
    4. Change from a Project to a Workspace
  3. Compile with Cloudant Sync
    1. Learning Objectives
    2. Create the CDTDatastore Bridging Header
    3. Check the Build
  4. Store Data Locally with Cloudant Sync
    1. Offline First
    2. Learning Objectives
    3. The Cloudant Document Model
    4. Design Plan
    5. Remove NSCoding
    6. Initialize the Cloudant Sync Datastore
    7. Side Note: Deleting the Datastore in the iOS Simulator
    8. Implement Storing and Querying Meals
    9. Create Sample Meals in the Datastore
  5. Conclusion
  6. Download This Project

Getting Started

FoodTracker main screen
FoodTracker main screen

These lessons assume that you have completed the FoodTracker app from Apple’s walk-through. First, complete that walk-through. It will teach you the process of beginning an iOS app and it will end with the chapter, Persist Data. Download the sample project from the final lesson (the “Download File” link at the bottom of the page).

Extract the zip file, Start-Dev-iOS-Apps-10.zip, browse into its folder with Finder, and double-click FoodTracker.xcodeproj. That will open the project in Xcode. Run the app (Command-R) and confirm that it works correctly. If everything is in order, proceed with this document.

CocoaPods

The first step is to install CocoaPods which will allow you to quickly and easily use open source packages in your iOS apps. You will use the CocoaPods repository to integrate the Cloudant Sync Datastore library, called CDTDatastore.

Learning Objectives

At the end of the lesson, you’ll be able to:

  1. Install CocoaPods on your Mac
  2. Use CocoaPods to download and integrate CDTDatastore with FoodTracker

Install CocoaPods on your Mac

The CocoaPods web site has an excellent page, Getting Started, which covers installing and upgrading. For your purposes, you will use the most simple approach to installation, the command-line gem program.

To install CocoaPods

  1. Open the Terminal application
    1. Click the Spotlight icon (a magnifying glass) in the Mac OS task bar
    2. Type “terminal” in the Spotlight prompt, and press return
  2. In Terminal, type this command:

    gem install cocoapods
    

    Note, if you receive an error message and the CocoaPods gem does not install, try this instead:

    sudo gem install cocoapods
    
  3. Confirm that CocoaPods is installed with this command:

    pod --version
    

    You should see the CocoaPods version displayed in Terminal:

    0.39.0
    

Install Cloudant Sync using CocoaPods

To install CDTDatastore as a dependency, create a Podfile, a simple configuration files which tell CocoaPods which packages this project needs.

To create a Podfile

  1. Choose File > New > File… (or press Command-N)
  2. On the left side of the dialog that appears, under “iOS”, select Other.
  3. Select Empty, and click Next.
  4. In the Save As field, type Podfile.
  5. The save location (“Where”) defaults to your project directory.

    The Group option defaults to your app name, FoodTracker.

    In the Targets section, make sure both your app and the tests for your app are not selected.

  6. Click Create. Xcode will create a file called Podfile which is open in the Xcode editor.

Next, configure CDTDatastore in the Podfile.

To configure the Podfile

  1. In Podfile, add the following code
    platform :ios, '9.1'
    pod "CDTDatastore", '~> 1.0.0'
    
  2. Choose File > Save (or press Command-S)

With your Podfile in place, you can now use CocoaPods to install the CDTDatastore pod.

To install CDTDatastore

  1. Open Terminal
  2. Change to your project directory, the directory containing your new Podfile. For example,
    # Your 'cd' command may be different; change to the folder you use.
    cd "FoodTracker - Persist Data"
    
  3. Type this command. Note, *this may take a few minutes to complete.

    pod install --verbose
    

You will see colorful output from CocoaPods in the terminal.

Running the pod install command

Output of the pod install command

Change from a Project to a Workspace

Because you are now integrating FoodTracker with the third-party CDTDatastore library, your project is now a group of projects combined into one useful whole. XCode supports this, and CocoaPods has already prepared you for this transition by creating FoodTracker.xcworkspace for you—a workspace encompassing both FoodTracker and CDTDatastore.

To change to your project workspace

  1. Choose File > Close Window (or press Command-W).
  2. Choose File > Open (or press Command-O).
  3. Select FoodTracker.xcworkspace and click Open.

You will see a similar XCode view as before, but notice that you now have two projects now.

FoodTracker workspace has two projects

Note, when you build or run the app, you may see compiler warnings from CDTDatastore code and its dependencies. You can safely ignore these warnings.

Checkpoint: Run your app. The app should behave exactly as before. Now you know that everything is in its place and working correctly.

Compile with Cloudant Sync

Your next step is to compile FoodTracker along with CDTDatastore. You will not change any major FoodTracker code yet; however, this will confirm that CDTDatastore and FoodTracker integrate and compile correctly.

Learning Objectives

At the end of the lesson, you’ll be able to create a bridging header to link Swift and Objective-C code.

Create the CDTDatastore Bridging Header

CDTDatastore is written in Objective-C. FoodTracker is a Swift project. Currently, the best way to integrate these projects together is with a bridging header. The bridging header, CloudantSync-Bridging-Header.h, will tell Xcode to compile CDTDatastore into the final app.

To create a header file

  1. Choose File > New > File (or press Command-N)
  2. On the left side of the dialog that appears, under “iOS”, select Source.
  3. Select Header File, and click Next.
  4. In the Save As field, type CloudantSync-Bridging-Header.
  5. Click the down-arrow expander button to the right of the “Save As” field. This will display the file system tree of the project.
  6. Click the FoodTracker folder.
  7. Confirm that the Group option defaults to your app name, FoodTracker.
  8. In the Targets section, check the FoodTracker target.
  9. Click Create. Xcode will create and open a file called CloudantSync-Bridging-Header.h.
  10. Under the line which says #define CloudantSync_Bridging_Header_h, insert the following code:
    #import <CloudantSync.h>
    
  11. Choose File > Save (or press Command-S)

The header file contents are done. But, despite its name, this file is not yet a bridging header as far as Xcode knows. The final step is to tell Xcode that this file will serve as the Objective-C bridging header.

To assign a project bridging header

  1. Enter the Project Navigator view by clicking the upper-left folder icon (or press Command-1).
  2. Select the FoodTracker project in the Navigator.
  3. Under Project, select the FoodTracker project. (It has a blue icon).
  4. Click “Build Settings”.
  5. Click All to show all build settings
  6. In the search bar, type “bridging header.” You should see Swift Compiler – Code Generation and inside it, Objective-C Bridging Header.

    Finding the bridging header value

  7. Double-click the empty space in the “FoodTracker” column, in the row “Objective-C Bridging Header”.
  8. A prompt window will pop up. Input the following:

    FoodTracker/CloudantSync-Bridging-Header.h
    

    Input the bridging header value

  9. Press return

Your bridging header is done! Xcode should look like this:

Final bridging header setup

Check the Build

Checkpoint: Run your app. This will confirm that the code compiles and runs. While you have not changed any user-facing app code, you have begun the first step to Cloudant Sync by compiling CDTDatastore into your project.

Store Data Locally with Cloudant Sync

With CDTDatastore compiled and connected to FoodTracker, the next step is to replace the NSCoder persistence system with CDTDatastore. Currently, in MealTableViewController.swift, during initialization, the encoded array of meals is loaded from local storage. When you add or change a meal, the entire meals array is encoded and stored on disk.

You will replace that system with a document-based architecture—in other words, each meal will be one record (called a “document” or simply “doc”) in the Cloudant Sync datastore.

Keep in mind, this first step of using Cloudant Sync does not use the Internet at all. The first goal is simply to store app data locally, in CDTDatastore. After that works correctly, you will add the ability to sync with Cloudant.

Offline First

This is the offline-first architecture, with Internet access being optional to use the app. All data operations are on the local device. If the device has an Internet connection, then the app will sync its data with Cloudant—covered in future posts in this series.

Learning Objectives

At the end of the lesson, you’ll be able to:

  1. Understand the Cloudant document model:
    1. Key-value storage for simple data types
    2. Attachment storage for binary data
    3. The document ID and revision ID
  2. Store meals in the Cloudant Sync datastore
  3. Query for meals in chronological order, from the datastore

The Cloudant Document Model

Let’s begin with a discussion of Cloudant basics. The document is the primary data model of the Cloudant database, not only CDTDatastore for iOS, but also for Android, the Cloudant hosted database, and even the open source Apache CouchDB database.

A document, often called a doc, is a set of key-value data. Do not think, “Microsoft Office document”; think “JSON object.” A document is a JSON object: keys (strings) can have values: Ints, Doubles, Bools, Strings, as well as nested Arrays and Dictionaries.

Documents can also contain binary blobs, called attachments. You can add, change, or remove attachments in a very similar way as you would add, change, or remove key-value data in a doc.

All documents always have two pieces of metadata used to manage them. The document ID (sometimes called _id or simply id) is a unique string identifying the doc. You use the ID to read, and write a specific document. When you create a document, you may omit the _id value, in which case Cloudant will automatically generate a unique ID for the document.

The revision ID (sometimes called _rev or revision) is a string generated by the datastore which tracks when the doc changes. The revision ID is mostly used internally by the datastore, especially to facilitate replication. In practice, you need to remember the basics about revisions:

  • The revision ID changes every time you update a document.
  • When you update a document, you provide the current revision ID to the datastore, and the datastore will return to you the new revision ID of the new document.
  • When you create a document, you do not provide a revision ID, since there is no such “current” document.

Finally, note that deleting a document is actually an update, with metadata set to indicate deletion, called a tombstone. Since a delete is an update just like any other, the deleted document will have its own revision ID. The tombstones are necessary for replication: replicating a tombstone from one database to another will cause doc to be deleted in both databases. As far as your app is concerned, it can consider the document deleted).

Design Plan

With this in mind, consider: how will the sample meals that are pre-loaded into the app work? At first, you might think to create meal documents when FoodTracker starts. That will work correctly the first time the user runs the app; however, if the user changes or deletes the sample meals, those changes must persist. For example, if the user deletes the sample meals and then restarts the app later, those meals must remain deleted.

To support this requirement, you will use document tombstones. This will be the basic design:

  • Each meal will be represented by a single document. User-created meals will have an automatically-generated document ID; but sample meals will have hard-coded document IDs: “meal1”, “meal2”, and “meal3”.
    // An example meal document:
    {
        "_id": "meal1",
        "name": "Caprese Salad",
        "rating": 4,
        "created_at": "2016-01-03T02:15:49.727Z"
    }
    
  • Sample meals have a hard-coded docId. Just before creating a sample meal, first try to fetch the meal by ID.
    • If CDTDatastore returns a meal doc, that means it has already been created. Do nothing.
    • If CDTDatastore returns a "not_found" error, that means the meal has never been created. Proceed with doc creation.
    • If CDTDatastore returns a different error, that means the meal has been created and then deleted. Do nothing.

Now, you can put this understanding into practice by transitioning to Cloudant Sync for local app data storage.

Remove NSCoding

Begin cleanly by removing the current NSCoding system from the model and the table view controller.

To remove NSCoding from the model

  1. Open Meal.swift
  2. Find the class declaration, which says
    class Meal: NSObject, NSCoding {
    
  3. Remove the word NSCoding and also the comma before it, making the new class declaration look like this:

    class Meal: NSObject {
    
  4. Delete the comment line, // MARK: NSCoding.
  5. Delete the method below that, encodeWithCoder(_:).
  6. Delete the method below that, init?(_:).

Next, remove NSCoding from the table view controller.

To remove NSCoding from the table view controller

  1. Open MealTableViewController.swift
  2. Find the method viewDidLoad(), and delete the comment beginning // Load any saved meals and also the if/else code below it:
    // Load any saved meals, otherwise load sample data.
    if let savedMeals = loadMeals() {
        meals += savedMeals
    } else {
        // Load the sample data.
        loadSampleMeals()
    }
    
  3. Delete the method loadSampleMeals(), which is immediately beneath the viewDidLoad() method.
  4. Find the method tableView(_:commitEditingStyle:forRowAtIndexPath:) and delete the line of code saveMeals().
  5. Find the method unwindToMealList(_:) and delete its last two lines of code: a comment, and a call to saveMeals().

    // Save the meals.
    saveMeals()
    
  6. Delete the comment line, // MARK: NSCoding
  7. Delete the method below that, saveMeals().
  8. Delete the method below that, loadMeals().

Checkpoint: Run your app. The app will obviously lose some functionality: loading stored meals, and creating the first three sample meals; although you can still create, edit, and remove meals (but they will not persist if you quit the app). That is okay. In the next step, you will restore these functions using Cloudant Sync instead.

Initialize the Cloudant Sync Datastore

Now you will add loading and saving back to the app, using the Cloudant Sync datastore. A meal will be a document, with its name and rating stored as key-value data, and its photo stored as an attachment. Additionally, you will store a creation timestamp, so that you can later sort the meals in the order they were created.

Begin with the Meal model, the file Meal.swift. You will add a new initialization method which can create a Meal object from a document. In other words, the init() method will set the meal name and rating from the document key-value data; and it will set the meal photo from the document attachment.

Representing a Meal as a Cloudant document requires few changes besides the initialization function. The only change to the the actual model is to add variables for the underlying document ID, and the creation time. By remembering a meal’s document ID, you will be able to change that doc when the user changes the meal (e.g. by changing its rating, its name, or its photo). And by storing its creation time, you can later query the database for meals in the order that the user created them.

To add Cloudant Sync datastore support

  1. Open Meal.swift
  2. In Meal.swift, in the section MARK: Properties, append these lines so that the variable declarations look like this:
    // MARK: Properties
    
    var name: String
    var photo: UIImage?
    var rating: Int
    
    // Data for Cloudant Sync
    var docId: String?
    var createdAt: NSDate
    
  3. In Meal.swift, edit the init?(_:photo:rating:) method to accept docId as a final argument, and to set the docId and createdAt properties. When you are finished, the method will look like this:

    init?(name: String, photo: UIImage?, rating: Int, docId: String?) {
        // Initialize stored properties.
        self.name = name
        self.photo = photo
        self.rating = rating
        self.docId = docId
        self.createdAt = NSDate()
    
        super.init()
    
        // Initialization should fail if there is no name or if the
        // rating is negative.
        if name.isEmpty || rating < 0 {
            return nil
        }
    }
    

Now add a convenience initializer. This initializer will use a given CDTDatastore document to create a Meal object.

To create a convenience initializer

  1. Open Meal.swift
  2. In Meal.swift, below the method init?(_:photo:rating:docId:), add the following code:
    required convenience init?(aDoc doc:CDTDocumentRevision) {
        if let body = doc.body {
            let name = body["name"] as! String
            let rating = body["rating"] as! Int
    
            var photo: UIImage? = nil
            if let photoAttachment = doc.attachments["photo.jpg"] {
                photo = UIImage(
                  data: photoAttachment.dataFromAttachmentContent())
            }
    
            self.init(name:name, photo:photo, rating:rating,
                      docId:doc.docId)
        } else {
            print("Error initializing meal from document: (doc)")
            return nil
        }
    }
    

That’s it for the model. The Meal class now tracks its underlying document ID and creation time; and it supports convenient initialization directly from a meal document.

Since the Meal model initializer has a new docId: String? parameter, you will need to update the one bit of code which initializes Meal objects, in the Meal view controller.

To update the meal view controller

  1. Open MealViewController.swift
  2. In MealViewController.swift, find the function prepareForSegue(_:sender:) and change the last section of code to (dd , docId: docId):
    // Set the meal to be passed to MealTableViewController after the
    // unwind segue.
    let docId = meal?.docId
    meal = Meal(name: name, photo: photo, rating: rating, docId: docId)
    

Now the model has been updated to work from Cloudant Sync documents.

Checkpoint: Run your app. The app should build successfully. This will confirm that all changes are working together harmoniously. Of course, the app behavior is obviously incomplete, which you will correct in the next steps.

All that remains is to use the datastore from the Meal table view controller. Begin by initializing the datastore and data.

To initialize the datastore

  1. Open MealTableViewController.swift
  2. In MealTableViewController.swift, in the section MARK: Properties, append these lines so that the variable declarations look like this:
    // MARK: Properties
    
    var meals = [Meal]()
    var datastoreManager: CDTDatastoreManager?
    var datastore: CDTDatastore?
    
  3. In MealTableViewController.swift, append the following code at the end of the method viewDidLoad():

    // Initialize the Cloudant Sync local datastore.
    initDatastore()
    

Now write the initialization function. Begin by creating a code marker for the new Cloudant Sync datastore methods.

To create a code marker for your code

  1. Open MealTableViewController.swift
  2. In MealTableViewController.swift, find the last method in the class, unwindToMealList(_:)
  3. Below that method, add the following:
    // MARK: Datastore
    

This will be the section of the code where you implement all Cloudant Sync datastore functionality.

To implement datastore initialization, in MealTableViewController.swift, append the following code in the section MARK: Datastore:

func initDatastore() {
    let fileManager = NSFileManager.defaultManager()

    let documentsDir = fileManager.URLsForDirectory(.DocumentDirectory,
        inDomains: .UserDomainMask).last!

    let storeURL = documentsDir.URLByAppendingPathComponent("foodtracker-meals")
    let path = storeURL.path

    do {
        datastoreManager = try CDTDatastoreManager(directory: path)
        datastore = try datastoreManager!.datastoreNamed("meals")
    } catch {
        fatalError("Failed to initialize datastore: (error)")
    }
}

Side Note: Deleting the Datastore in the iOS Simulator

Sometimes during development, you may want to delete the datastore and start over. There are several ways to do this, for example, by deleting the app from the simulated device.

However, here is a quick command you can paste into the terminal. It will remove the Cloudant Sync database. When you restart the app, the app will initialize a new datastore and behave as if this was its first time to run. For example, it will re-create the sample meals again.

To delete the datastore from the iOS Simulator

rm -i -rv $HOME/Library/Developer/CoreSimulator/Devices/*/data/Containers/Data/Application/*/Documents/foodtracker-meals

This command will prompt you to remove the files. If you are confident that the command is working correct, you can omit the -i option.

Implement Storing and Querying Meals

With the datastore initialized, you need to write methods to store and retrieve meal documents. This is the cornerstone of your project. With a few methods to interact with the datastore, you will enjoy all the benefits the Cloudant Sync datastore brings: offline-first operation and cloud syncing.

For FoodTracker, you will have two primary ways of persisting meals in the datastore: creating meals and updating meals. Each of these will have its own method, but the methods will share some common code to populate a meal document with the correct data. Begin by writing this method. Given a Meal object and a Cloudant document, it will copy all of the meal data to the document, so that the latter can be created or updated as needed.

To implement populating a meal document

  1. Open MealTableViewController.swift
  2. In MealTableViewController.swift, in the section MARK: Datastore, append a new method:
    func populateRevision(meal: Meal, revision: CDTDocumentRevision?) {
       // Populate a document revision from a Meal.
       let rev: CDTDocumentRevision = revision
           ?? CDTDocumentRevision(docId: meal.docId)
       rev.body["name"] = meal.name
       rev.body["rating"] = meal.rating
    
       // Set created_at as an ISO 8601-formatted string.
       let dateFormatter = NSDateFormatter()
       dateFormatter.locale = NSLocale(localeIdentifier: "en_US_POSIX")
       dateFormatter.timeZone = NSTimeZone(abbreviation: "GMT")
       dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
       let createdAtISO = dateFormatter.stringFromDate(meal.createdAt)
       rev.body["created_at"] = createdAtISO
    
       if let data = UIImagePNGRepresentation(meal.photo!) {
           let attachment = CDTUnsavedDataAttachment(data: data,
               name: "photo.jpg", type: "image/jpg")
           rev.attachments[attachment.name] = attachment
       }
    }
    

Next, implement the method to create new meal documents. Note that sample meals will have hard-coded document IDs, so that you can detect if they have already been created or not. User-created meals will have no particular doc ID.

To implement meal document creation

  1. In MealTableViewController.swift, in the section MARK: Datastore, append a new method:
    // Create a meal. Return true if the meal was created, or false if
    // creation was unnecessary.
    func createMeal(meal: Meal) -> Bool {
       // User-created meals will have docId == nil. Sample meals have a
       // string docId. For sample meals, look up the existing doc, with
       // three possible outcomes:
       //   1. No exception; the doc is already present. Do nothing.
       //   2. The doc was created, then deleted. Do nothing.
       //   3. The doc has never been created. Create it.
       if let docId = meal.docId {
           do {
               try datastore!.getDocumentWithId(docId)
               print("Skip (docId) creation: already exists")
               return false
           } catch let error as NSError {
               if (error.userInfo["NSLocalizedFailureReason"] as? String
                       != "not_found") {
                   print("Skip (docId) creation: already deleted by user")
                   return false
               }
    
               print("Create sample meal: (docId)")
           }
       }
    
       let rev = CDTDocumentRevision(docId: meal.docId)
       populateRevision(meal, revision: rev)
    
       do {
           let result = try datastore!.createDocumentFromRevision(rev)
           print("Created (result.docId) (result.revId)")
       } catch {
           print("Error creating meal: (error)")
       }
    
       return true
    }
    

Now you are ready to write the update method. Note that “deleting” a Cloudant document is in fact a type of update. The update method will accept a Bool parameter indicating whether to delete the document or not. However, to keep the rest of the code simple, you will write one-line convenience methods deleteMeal(_:) and updateMeal(_:) to set the deletion flag automatically.

To implement deleting and updating meal documents

  1. In MealTableViewController.swift, in the section MARK: Datastore, append the two convenience methods and then the full implementation.
    func deleteMeal(meal: Meal) {
        updateMeal(meal, isDelete: true)
    }
    
    func updateMeal(meal: Meal) {
        updateMeal(meal, isDelete: false)
    }
    
    func updateMeal(meal: Meal, isDelete: Bool) {
        guard let docId = meal.docId else {
            print("Cannot update a meal with no document ID")
            return
        }
    
        let label = isDelete ? "Delete" : "Update"
        print("(label) (docId): begin")
    
        // First, fetch the current document revision from the DB.
        var rev: CDTDocumentRevision
        do {
            rev = try datastore!.getDocumentWithId(docId)
            populateRevision(meal, revision: rev)
        } catch {
            print("Error loading meal (docId): (error)")
            return
        }
    
        do {
            var result: CDTDocumentRevision
            if (isDelete) {
                result = try datastore!.deleteDocumentFromRevision(rev)
            } else {
                result = try datastore!.updateDocumentFromRevision(rev)
            }
    
            print("(label) (docId) ok: (result.revId)")
        } catch {
            print("Error updating (docId): (error)")
            return
        }
    }
    

Your app can now create, update, and delete meal docs. To complete this feature, these methods must be integrated with UI. When the user saves or deletes a meal, the controller must run these methods.

To create and update meals

  1. In MealTableViewController.swift, in the method unwindToMealList(_:), modify the method body so that it calls updateMeal() or createMeal() as appropriate. The code will look as follows:
    if let selectedIndexPath = tableView.indexPathForSelectedRow {
        // Update an existing meal.
        meals[selectedIndexPath.row] = meal
        tableView.reloadRowsAtIndexPaths([selectedIndexPath], withRowAnimation: .None)
        updateMeal(meal)
    } else {
        // Add a new meal.
        let newIndexPath = NSIndexPath(forRow: meals.count, inSection: 0)
        meals.append(meal)
        tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom)
        createMeal(meal)
    }
    
  2. In the method tableView(_:commitEditingStyle:forRowAtIndexPath), insert a call to deleteMeal(_:) for the .Delete editing event. The code will look as follows.

    if editingStyle == .Delete {
        // Delete the row from the data source
        let meal = meals[indexPath.row]
        deleteMeal(meal)
        meals.removeAtIndex(indexPath.row)
        tableView.deleteRowsAtIndexPaths([indexPath],
            withRowAnimation: .Fade)
    

The final thing to write is the code to query for meals in the datastore. This code has two parts: initializing an index during app startup (to query by timestamp), and of course the code to query that index.

To support querying meals by timestamp

  1. In MealTableViewController.swift, in the method initDatastore(), append this code:
    datastore?.ensureIndexed(["created_at"], withName: "timestamps")
    
    // Everything is ready. Load all meals from the datastore.
    loadMealsFromDatastore()
    
  2. In MealTableViewController.swift, in the section MARK: Datastore, append this method:

    func loadMealsFromDatastore() {
        let query = ["created_at": ["$gt":""]]
        let result = datastore?.find(query, skip: 0, limit: 0, fields:nil, sort: [["created_at":"asc"]])
        guard result != nil else {
            print("Failed to query for meals")
            return
        }
    
        meals.removeAll()
        result!.enumerateObjectsUsingBlock({ (doc, idx, stop) -> Void in
            if let meal = Meal(aDoc: doc) {
                self.meals.append(meal)
            }
        })
    }
    

That’s it! The most intricate part of your code is finished.

Create Sample Meals in the Datastore

Now is time to create sample meal documents during app startup. This method will run every time the app initializes. For each sample meal, it will call createMeal(_:) which will either create the documents or no-op, as needed.

To create sample meals during app startup

  1. In MealTableViewController.swift, in the section MARK: Datastore, add a new method:
    func storeSampleMeals() {
        let photo1 = UIImage(named: "meal1")!
        let photo2 = UIImage(named: "meal2")!
        let photo3 = UIImage(named: "meal3")!
    
        let meal1 = Meal(name: "Caprese Salad", photo: photo1, rating: 4,
            docId: "sample-1")!
        let meal2 = Meal(name: "Chicken and Potatoes", photo: photo2, rating: 5,
            docId:"sample-2")!
        let meal3 = Meal(name: "Pasta with Meatballs", photo: photo3, rating: 3,
            docId:"sample-3")!
    
        // Hard-code the createdAt property to get consistent revision IDs. That way, devices that share
        // a common cloud database will not generate conflicts as they sync their own sample meals.
        let comps = NSDateComponents()
        comps.day = 1
        comps.month = 1
        comps.year = 2016
        comps.timeZone = NSTimeZone(abbreviation: "GMT")
        let newYear = NSCalendar.currentCalendar()
            .dateFromComponents(comps)!
    
        meal1.createdAt = newYear
        meal2.createdAt = newYear
        meal3.createdAt = newYear
    
        createMeal(meal1)
        createMeal(meal2)
        createMeal(meal3)
    }
    
  2. In MealTableViewController.swift, in the method initDatastore(), insert a call to storeSampleMeals() before the code initializing the index. The final lines of the method will look as follows:

       storeSampleMeals()
       datastore?.ensureIndexed(["created_at"], withName: "timestamps")
    
       // Everything is ready. Load all meals from the datastore.
       loadMealsFromDatastore()
    }
    

Checkpoint: Run your app. The app should behave exactly as it did at the beginning of this project.

Conclusion

Congratulations! While the app remains unchanged superficially, you have made a very powerful upgrade to FoodTracker’s most important aspect: its data. You have transformed the data layer from a minimal, unexceptional side note to become a flexible, powerful database. This database can be queried, searched, scaled, and replicated between devices and through the cloud.

The next update of this series will cover replicating this data to the cloud using IBM Cloudant. Indeed, implementing cloud syncing is much simpler than the work from this lesson. You have completed laying the foundation!

Download This Project

To see the completed sample project for this lesson, download the file and view it in Xcode.

Download File

4 comments on"Offline-First iOS Apps with Swift & Cloudant Sync; Part 1: The Datastore"

  1. Manish Kumar January 23, 2018

    In case you are running into issues:

    1. Change the contents of Podfile to:

    platform :ios, ‘10.0’

    target ‘FoodTracker’ do

    pod ‘CDTDatastore’, ‘~> 1.0.0’

    end

    2. If you get the error bridging header not found, add Pods/** to the header search path under build settings.

  2. Note the Podfile must have ‘end’ as the last line of the file.

  3. Podfile must contain all this:

    target ‘Food Tracker’ do
    source ‘https://github.com/CocoaPods/Specs.git’

    platform :ios, ‘10.3’
    pod “CDTDatastore”

    end

  4. Great tutorial!
    Some things have changed. Using Xcode 10 I came across the following:

    Section “To create a convenience initializer”
    This line (in Meal.swift): if let body = doc.body
    Triggers error: Initializer for conditional binding must have Optional type, not ‘NSMutableDictionary’
    Solution: I removed the if else statement, and replaced it with: let body = doc.body

    This line: photo = UIImage(data: photoAttachment.dataFromAttachmentContent())
    Triggers: Value of type ‘Any’ has no member ‘dataFromAttachmentContent’
    Solution: Use Fix function to: data: (photoAttachment as AnyObject).dataFromAttachmentContent()!)

    Section “To implement datastore initialisation”
    Trigger: Xcode points out that ‘NSFileManager’ has been renamed to ‘FileManager’
    This first three lines should be changed to these two lines:
    let documentsDir = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
    let storeURL = documentsDir.appendingPathComponent(“foodtracker-meals”)
    (See explanation at https://stackoverflow.com/questions/38878843/swift-3-nsfilemanager-xcode-8-error-has-been-renamed-filemanager)

    Section: To implement populating a meal document:
    let rev: CDTDocumentRevision = revision
    ?? CDTDocumentRevision(docId: meal.docId)
    Triggers: Value of optional type ‘String?’ must be unwrapped to a value of type ‘String’
    Solution: use Fix

    let dateFormatter = NSDateFormatter()
    Triggers: ‘NSDateFormatter’ has been renamed to ‘DateFormatter’
    Solution: use Fix (and a number of times on lines below as well)

    Section: To implement meal document creation:
    Line: let rev = CDTDocumentRevision(docId: meal.docId)
    Trigger: Value of optional type ‘String?’ must be unwrapped to a value of type ‘String’
    Solution: use Fix (including the lines below)

    Selection: deleting and updating document.
    Use Fix to fix the errors.

    Section: To create sample meals during app startup
    Various errors that you can solve with the Fix function.
    But I wasn’t able to quickly fix the NSDateComponets part. So I commented out the whole part from
    let comps = NSDateComponents()
    To
    Let newYear…
    And just set the mealx createAt dates this way (probably going to give me trouble later on though).

    meal1.createdAt = NSDate();
    meal2.createdAt = NSDate();
    meal3.createdAt = NSDate();

Join The Discussion

Your email address will not be published. Required fields are marked *