Navigation

Build the To-Do Client

Deployment Type:

Author: MongoDB Documentation Team

This tutorial walks you through building an app that uses Stitch for the backend, and enables users to create and maintain to-do lists.

Time required: 20 minutes

What You’ll Need

If you have not yet built the To-do backend, follow the steps in this guide.

Procedure

Now that you’ve set up a Stitch backend for your to-do application, you need a client application so that users can add, view, and complete to-do tasks. To help you get started, we’ve created client applications that include all the files and components you’ll need to follow this tutorial.

A. Get the Base To-Do Client Application

Estimated Time to Complete: ~10 minutes

1

Clone the Client App Repository

To get a local copy of the client application GitHub repository on your computer, clone the repo by running the following command in a terminal:

git clone git@github.com:mongodb-university/stitch-tutorial-todo-web.git
2

Install the App’s Dependencies

After you’ve cloned the repository to your computer, run the following code in your terminal to navigate into the repo and install the app’s dependencies:

cd stitch-tutorial-todo-web
npm install

Stitch Browser JS SDK

The base application that you cloned already lists the Stitch Browser SDK as a dependency so this step installs the SDK along with the other dependencies. For other applications, you can install the SDK from npm with the following command:

npm install --save mongodb-stitch-browser-sdk
3

Explore the Code and File Structure

The web client is a standard React web application scaffolded with create-react-app. We encourage you to explore the files in the app for a few minutes before you continue the tutorial. This will help you to familiarize yourself with what the app contains and where you’ll be working.

Stitch

The /src/stitch directory contains all of the code you’ll use to connect the application to Stitch. Each file in this directory exports functions and objects that encapsulate the Stitch component it’s named after. These files are only incomplete scaffolds. This tutorial walks through adding the missing code in these files to connect the app to Stitch.

React Components

The /src/components directory contains pre-built React components based on components from the reactstrap library and styled with Emotion. The components import code from the files in /src/stitch and use them to interact with Stitch. We’ve already completely implemented the React components so you won’t need to add any code to the component files.

React Components

React is a popular modern web application framework that uses a component model to maintain application state and intelligently render pieces of the UI. If you’re not familiar with React or want to brush up on your knowledge, check out the official React website which has excellent documentation and tutorials.

B. Integrate Your Stitch App

Estimated Time to Complete: ~10 minutes

Once you’re a little more familiar with the app’s file structure it’s time to get down to business and connect the app to Stitch. In this section you’ll be adding code to the following files in /src/stitch:

  • app.js
  • mongodb.js
  • authentication.js

After you complete this section the to-do client application will be connected to your Stitch backend and you’ll be able to add, complete, and hide to-do tasks through the app.

1

Connect to Stitch

The first thing that you need to do is connect the client application to your Stitch backend. In this step, you’ll instantiate an app client that provides objects and methods for interacting with Stitch. You need an app client to authenticate users, call Functions, and instantiate service clients.

To get started, navigate to the stitch-tutorial-todo-web repository and open the file /src/stitch/app.js. The file is incomplete and you will need to add some code to make the app work. The file should look like this when you first open it:

/src/stitch/app.js
1
2
3
4
5
6
7
8
9
import { Stitch } from "mongodb-stitch-browser-sdk";

// TODO: Add your Stitch app's App ID
const APP_ID = "<YOUR APP ID>";

// TODO: Initialize the app client
const app =

export { app };

At the top of the file we import an object named Stitch from the Stitch Browser SDK. This object contains methods that instantiate and manage Stitch app clients. Each app client is associated with a single Stitch app that you specify by providing an App ID when you instantiate the client. At the bottom of the file we export the app object, which should contain an app client for your Stitch app.

There are two things you need to do to connect the to-do client to Stitch:

  1. Replace <YOUR APP ID> with the App ID of your Stitch to-do app.

    Find Your App ID

    You can find your Stitch app’s App ID at the top of the left-hand navigation in the Stitch UI. Click the Copy button ( copy icon ) to copy it to your clipboard.

    The App ID of a to-do app in the Stitch UI navigation
  2. Instantiate an app client in the app variable by adding the following code:

    7
    8
    9
    const app = Stitch.hasAppClient(APP_ID)
      ? Stitch.getAppClient(APP_ID)
      : Stitch.initializeAppClient(APP_ID);
    

The Stitch object stores initialized app clients and throws an error if you attempt to initialize a second client with the same App ID, so we use a ternary expression and three methods on the Stitch object to create an app client. The ternary checks to see if an app client has already been inititialized and, if so, uses that instead of initializing a new app client.

Once you’ve added the code to app.js your client application is connected to Stitch! In the next steps you’ll use the app that you initialized and exported to actually interact with Stitch and MongoDB Atlas.

2

Instantiate a MongoDB Collection Handle

The To-do app stores to-do items in the MongoDB collection todo.items. You already defined rules for the collection in your Stitch app, so all you need to start running queries is a collection object that exposes MongoDB actions.

To get a handle for the todo.items collection, open the file /src/stitch/mongodb.js. The file should look like this when you first open it:

/src/stitch/mongodb.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import { RemoteMongoClient } from "mongodb-stitch-browser-sdk";
import { app } from "./app";

// TODO: Initialize a MongoDB Service Client
const mongoClient =

// TODO: Instantiate a collection handle for todo.items
const items =

export { items };

At the top of the file we import a RemoteMongoClient object from the Stitch Browser SDK and the app that you defined and exported from app.js. The SDK will use the RemoteMongoClient to create a MongoDB service client that provides methods for interacting with your linked cluster. At the bottom of the file we export the items object, which should contain a collection handle that you can use to query the todo.items collection.

There are two things you need to do to create the collection handle:

  1. Instantiate a MongoDB service client in the mongoClient variable by adding the following code:

    5
    6
    7
    8
    const mongoClient = app.getServiceClient(
      RemoteMongoClient.factory,
      "mongodb-atlas"
    );
    

    MongoDB Service Name

    The default cluster If you chose to name your linked MongoDB Atlas cluster something other than the default name, make sure to use that name when you get the service client.

  2. Get the collection handle by adding the following code:

    11
    const items = mongoClient.db("todo").collection("items");
    

The app client provides a generic getServiceClient method that instantiates service clients for all Stitch services. To specify that the client is for a particular MongoDB service we provide a factory function from the RemoteMongoClient object and the name of the MongoDB service. The MongoDB service client that the method returns is similar to a MongoClient in a standard MongoDB driver. Given that, we can instantiate a collection handle by calling the db and collection methods.

How We Use It

In our React app, we use the collection handle that you instantiated to find, add, modify, and remove documents in the todo.items collection that represent a user’s to-do tasks. We write each query inside of a function that we return from the useTodoItems hook. Each function waits for its query to finish and then updates the React application state based on the query result by calling the dispatch function.

/src/components/useTodoItems.js
import { items } from "../stitch";
        
export function useTodoItems(userId) {
  const [state, dispatch] = React.useReducer(todoReducer, { todos: [] });
  const addTodo = async task => {
    const todo = { task, owner_id: userId };
    const result = await items.insertOne(todo);
    dispatch({ type: "addTodo", payload: { ...todo, _id: result.insertedId } });
  };
  const removeTodo = async todoId => {
    await items.deleteOne({ _id: todoId });
    dispatch({ type: "removeTodo", payload: { id: todoId } });
  };
  const completeAllTodos = async () => {
    await items.updateMany({ owner_id: userId }, { $set: { checked: true } });
    dispatch({ type: "completeAllTodos" });
  };

  // ... more to-do action definitions
}
3

Set Up Anonymous User Login

Now that you’ve added the code to mongodb.js, your client application has a way to query MongoDB. However, Stitch requires that all requests be made by a user that has logged in through an authentication provider. Stitch allows you to configure one or more authentication providers that you can use to authenticate your app’s users and log them in. You already enabled Anonymous authentication in your Stitch app, so all that’s left to do is use the app.auth object to set up the authentication functions that the to-do client calls.

To set up anonymous authentication in your client app, open the file /src/stitch/authentication.js. The file should look like this when you first open it:

/src/stitch/authentication.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import { AnonymousCredential } from "mongodb-stitch-browser-sdk";
import { app } from "./app.js";

export function loginAnonymous() {
  // TODO: Allow users to log in anonymously
}

export function hasLoggedInUser() {
  // TODO: Check if there is currently a logged in user
}

export function getCurrentUser() {
  // TODO: Return the user object of the currently logged in user
}

export function logoutCurrentUser() {
  // TODO: Logout the currently logged in user
}

At the top of the file we import an AnonymousCredential object from the Stitch Browser SDK and the app that you defined and exported from app.js. The AnonymousCredential is an object that we use to log in to Stitch as an anonymous user. The SDK also provides credential objects for each of the other authentication providers that Stitch supports, but you won’t need those for now. The file exports four incomplete functions that we use in our React components to handle user authentication.

To allow your users to log in and store to-do tasks in Stitch, you need to add code to make the exported authentication functions work.

The main thing you need to do is actually handle a user login request. To do so, add the following code to the loginAnonymous function:

4
5
6
7
8
export function loginAnonymous() {
  // Allow users to log in anonymously
  const credential = new AnonymousCredential();
  return app.auth.loginWithCredential(credential);
}

You’ll also need to check if a user is currently logged in. Add the following code to the hasLoggedInUser function:

10
11
12
13
export function hasLoggedInUser() {
  // Check if there is currently a logged in user
  return app.auth.isLoggedIn;
}

If there is a logged in user, we want to be able to access their user object. Add the following code to the getCurrentUser function:

15
16
17
18
export function getCurrentUser() {
  // Return the user object of the currently logged in user (if there is one)
  return app.auth.isLoggedIn ? app.auth.user : null;
}

A logged in user should be able to log out of the to-do app. Add the following code to the logoutCurrentUser function:

20
21
22
23
24
25
26
export function logoutCurrentUser() {
  // Logout the currently logged in user (if there is one)
  const user = getCurrentUser();
  if(user) {
    return app.auth.logoutUserWithId(user.id);
  }
}

How We Use It

In our React app, we use the authentication functions that you just completed to log users in and out, route logged out users to the login screen, and associate the logged in user’s id with their to-do tasks. We encapsulate this logic inside of a global app state that we modify using the handleAnonymousLogin and handleLogout functions. We expose the authentication state and functions to the app through React context in the StitchAuth component.

/src/components/StitchAuth.js
import {
  hasLoggedInUser,
  loginAnonymous,
  logoutCurrentUser,
  getCurrentUser
} from "./../stitch/authentication";

export function StitchAuthProvider(props) {
  // Set up our authentication state and initialize it using the
  // functions that we export from "/src/stitch/authentication.js"
  const [authState, setAuthState] = React.useState({
    isLoggedIn: hasLoggedInUser(),
    currentUser: getCurrentUser()
  });
  const handleAnonymousLogin = async () => {
    // Call the function we defined to log in to Stitch and then update
    // our authentication state
    if (!authState.isLoggedIn) {
      const loggedInUser = await loginAnonymous();
      setAuthState({
        ...authState,
        isLoggedIn: true,
        currentUser: loggedInUser
      });
    }
  };
  const handleLogout = async () => {
    // Call the function we defined to log out of Stitch and then update
    // our authentication state
    if (authState.isLoggedIn) {
      await logoutCurrentUser();
      setAuthState({
        ...authState,
        isLoggedIn: false,
        currentUser: null
      });
    }
  };
  // In the actual code we call useMemo() on the value to improve performance
  return (
    <StitchAuthContext.Provider value={{
      isLoggedIn: authState.isLoggedIn,
      currentUser: authState.currentUser,
      actions: { handleAnonymousLogin, handleLogout }
    }}>
      {props.children}
    </StitchAuthContext.Provider>
  );
}
4

Run the To-Do Client Application

Once you’ve added the code to authentication.js you should be able to log in and track to-do tasks. To run the app, navigate to the root of the stitch-tutorial-todo-web directory and run the following command:

npm start

The start command builds and serves a local copy of the application. Once the command output indicates that it is serving the app, open your browser to localhost:3000 to view the to-do client.

The app should show a login screen when you first start it. Click Log in Anonymously to log in as a new user and then add and check off some to-do tasks. If everything works, then you’ve successfully built the base to-do client!

A. Get the Base Client Application

To get started, clone the client application GitHub repository by running the following command in a terminal:

git clone git@github.com:mongodb-university/stitch-tutorial-todo-android.git

The client application repo uses tags to mark good places to start each tutorial. For this tutorial, run the following command to fetch the tutorial-1 tag:

git checkout tutorial-1

B. Build and Run the Application

1

Open the project in Android Studio

  1. Open Android Studio.
  2. Select Open an existing Android Studio project.
  3. Navigate to the downloaded example directory and click Open.
2

Update project with your MongoDB Stitch App ID

From the Project view:

  1. Go to the stitch-tutorial-todo-android/src/main/res/values folder and open the strings.xml file.
  2. Update the stitch_client_app_id with your MongoDB Stitch app id and save.

Find Your App ID

You can find your Stitch app’s App ID at the top of the left-hand navigation in the Stitch UI. Click the Copy button ( copy icon ) to copy it to your clipboard.

The App ID of a to-do app in the Stitch UI navigation
3

Initialize the App Client

You will now add the code needed to initialize the StitchAppClient. The StitchAppClient class provides all of the functionality for your app to communicate with the Stitch backend, including accessing the Authentication providers and Atlas service.

In the onCreate method of the TodoListActivity.java file, find the // 1. Instantiate the Stitch client comment, and uncomment the next line. Add the code to instantiate a StitchAppClient object. When complete, your code should look like the following:

@Override
protected void onCreate(final Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_todo_list);

// TODO:
// 1. Instantiate the Stitch client
client = Stitch.getDefaultAppClient();
4

Instantiate a MongoDB Client

For your app to communicate with MongoDB (Atlas), you need to create a RemoteMongoClient object.

In the onCreate method of the TodoListActivity.java file, find the // 2. Instantiate a RemoteMongoClient comment, and uncomment the next line. Add the code needed to create the RemoteMongoClient. You should call the getServiceClient() method on the client object, passing in the RemoteMongoClient.factory and the name of your MongoDB service.

Note

By default, the MongoDB service in Stitch is named “mongodb-atlas”. If you provided a different name when setting up your Stitch backend, be sure to use that name here.

When complete, your code should look like the following:

  @Override
  protected void onCreate(final Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_todo_list);

  // TODO:
  // 1. Instantiate the Stitch client
  client = Stitch.getDefaultAppClient();

  // 2. Instantiate a RemoteMongoClient
  final RemoteMongoClient mongoClient = client.getServiceClient(
     RemoteMongoClient.factory, "mongodb-atlas");

  // 3. Set up the items collection
  // items =
5

Create a RemoteMongoCollection

You now need to create a RemoteMongoDatabase object to point to the todo database, and then create a RemoteMongoCollection object that points specificallty to the items collection. Your code will then use the RemoteMongoCollection for all CRUD operations against the collection.

When you instantiate a RemoteMongoCollection object, you specify the name of the collection and the class that represents the data structure of the data stored in that collection. When storing a Java object in MongoDB, you need to define a custom Codec for your object, and pass that to the withCodecRegistry() function.

At the end of the TodoItem class, you will find the Codec we have created for the TodoItem object:

 public static final Codec<TodoItem> codec = new Codec<TodoItem>() {

   @Override
   public void encode(final BsonWriter writer, final TodoItem value,
                  final EncoderContext encoderContext) {
   new BsonDocumentCodec().encode(
           writer, toBsonDocument(value), encoderContext);
 }

   @Override
   public Class<TodoItem> getEncoderClass() {
       return TodoItem.class;
   }

   @Override
   public TodoItem decode(
       final BsonReader reader, final DecoderContext decoderContext) {
       final BsonDocument document = (new BsonDocumentCodec())
         .decode(reader, decoderContext);
       return fromBsonDocument(document);
   }
 };

In the onCreate method of the TodoListActivity.java file, find the // 3. Set up the items collection comment, and uncomment the next line. Add the code needed to call the getDatabase(), getCollection(), and withCodecRegistry() functions to create a RemoteMongoCollection object. When complete, your code should look like the following:

@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_todo_list);

// TODO:
// 1. Instantiate the Stitch client
client = Stitch.getDefaultAppClient();

// 2. Instantiate a RemoteMongoClient
final RemoteMongoClient mongoClient = client.getServiceClient(
   RemoteMongoClient.factory, "mongodb-atlas");

// 3. Set up the items collection
items = mongoClient
           .getDatabase(TodoItem.TODO_DATABASE)
           .getCollection(TodoItem.TODO_ITEMS_COLLECTION, TodoItem.class)
           .withCodecRegistry(CodecRegistries.fromRegistries(
                 BsonUtils.DEFAULT_CODEC_REGISTRY,
                 CodecRegistries.fromCodecs(TodoItem.codec)));
6

Run the todo app

  1. Click Run.

  2. If no virtual device is available, click Create New Virtual Device.

    1. Select the Phone device for your app. You can select any of the deviuce definitions.

    2. Click Next.

    3. On the System Image tab, you are presented with a list of Recommended images, all of which are x86-based. You must switch to the Other Images tab to see the full list of images, including those platforms supported by Stitch. Select one of the supported platforms (an ARM32, ARM64, or x86_64 device that is running an armeabi-v7a, arm64-v8a, or x86_64 image of Android 5.0 or later), and, if neccessary, click Download to download the image.

      Default images

      The list of Recommended images only contains x86 images, which Stitch does not support. You must switch to the Other Images tab to see the full list of images.

    4. Click Finish.

  3. Select the virtual device from the Available Virtual Devices.

  4. Click OK.

  5. Wait for the device to come online.

  6. Sign in using anonymous login.

A. Create a New Application

Optional Step

If you do not want to go through the intial project setup and would prefer to jump right into the code, you can clone the Stitch github example repository, or download the zip file for the repo and unzip it, then proceed to the next step.

Throughout this tutorial, if you wish to follow along and see the code you can do so by checking out git tags that correspond to the end of the previous step.

Every step will include information about which tag to checkout, like the following.

Git Tag

This step corresponds to git checkout step0

Estimated Time to Complete: ~4 minutes

1

Create a New Xcode Project.

Start Xcode, and create a new project by clicking on Create a new Xcode project or by navigating to File > New > Project.

../../../_images/xcode-welcome.png
2

Single View App.

On the template screen within Xcode, ensure iOS is selected as your platform, and select Single View App as the application type. Then click Next.

../../../_images/xcode-single-view-app.png
3

Name the Application.

Name the product Todo, and ensure Swift is selected as the language, then click Next.

../../../_images/xcode-todo-ios.png
4

Test Application Setup

You should now be greeted with the Xcode project window. Let’s go ahead and ensure that everything is correctly set up before we dive into using Stitch.

In the upper left corner of the window, ensure Todo is the selected project. For this tutorial we’ll be using the iPhone XR in the simulator. With these selected, click on the run button, and you should be greeted with a blank simulator screen.

../../../_images/xcode-run-bar.png
../../../_images/ios-simulator-blank.png

B. Build and Run the Application

Estimated Time to Complete: ~15 minutes

1

Install the Pods

Required Step

This is a required step and can not be skipped by following along with the git tags. The Pods are not included in source control.

To checkout the code at this point, enter git checkout step1

This project uses CocoaPods to manage dependencies, which means you need to install the pods before building the project. To do so, follow these steps:

  1. In a terminal window, navigate to the todo-ios directory.
  2. Run pod init
  3. In the newly created Podfile, add pod 'StitchSDK', '~> 6.0.0' on a new line after use frameworks!.
  4. Run pod install --repo-update to install the project dependencies.
2

Open the Project

After executing pod install, Cocoapods will generate a new workspace for your project called Todo.xcworkspace.

Open the workspace file Todo.xcworkspace:

open ToDoSync.xcworkspace

Your Xcode workspace should look similar to the following:

../../../_images/todo-ios-after-pod.png

Run the Todo app again to ensure everything is working correctly. You may see some build warnings, however, the iOS simulator should launch again and you should see another blank white screen.

3

Remove Unnecessary Files

To checkout the code at this point, enter git checkout step3

Let’s delete files and information that aren’t necessary.

  1. Open the Info.plist for your target.
  2. Find the Main storyboard file base name entry and remove it by pressing the - icon next to it in the property editor.
  3. Find the Main.storyboard file in Xcode’s navigator and delete it. Choose Move to Trash in the confirmation dialogue.
4

Scaffold the Naviation Controller

To checkout the code at this point, enter git checkout step4

Since we deleted our Main.storyboard, let’s set up our NavigationController to present a Welcome View which we’ll build out in a later step.

  1. Open AppDelegate.swift and find the first function:

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        return true
    }
    
  2. Replace this function with the following code.

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
    
        window = UIWindow(frame: UIScreen.main.bounds)
        window?.makeKeyAndVisible()
        window?.rootViewController = UINavigationController(rootViewController: WelcomeViewController())
    
        return true
    }
    

This defines the root view controller that will be presented to users when the application finishes launching. Xcode will show an error Use of unresolved identifier 'WelcomeViewController'.

5

Create a Constants File

To checkout the code at this point, enter git checkout step5

Next, let’s create a new file to contain our app’s constants. We’ll create a single source of reference to hold information about our database and collection name, as well as the Atlas Service name and our Stitch APP ID.

Find Your App ID

You can find your Stitch app’s App ID at the top of the left-hand navigation in the Stitch UI. Click the Copy button ( copy icon ) to copy it to your clipboard.

The App ID of a to-do app in the Stitch UI navigation
  1. Create a new Swift file by selecting File > New > File.. and selecting Swift File from the type selector panel. Press next, then enter Constants for the file name. Ensure the file is created and saved in your project.
  2. Paste the following code into Constants.swift. Ensure you update static let STITCH_APP_ID = ... to your own APP ID.
import Foundation
struct Constants {
    static let TODO_DATABASE = "todo"
    static let TODO_ITEMS_COLLECTION = "items"

    // your Atlas service name. On creation, this defauls to mongodb-atlas
    static let ATLAS_SERVICE_NAME = "mongodb-atlas"
    // your Stitch APP ID
    static let STITCH_APP_ID = "todo-ios-sqjba" // <- update this!
}

Note

By default, the MongoDB service in Stitch is named “mongodb-atlas”. If you provided a different name when setting up your Stitch backend, be sure to change ATLAS_SERVICE_NAME to match.

6

Initialize the Stitch App Client

To checkout the code at this point, enter git checkout step6

You will now add the code needed to initialize the StitchAppClient. The StitchAppClient class provides all of the functionality for your app to communicate with the Stitch backend, including accessing the Authentication providers and Atlas service.

To initialize the StitchAppClient, you will call the initializeAppClient() function and pass in the ID of your Stitch backend app.

Add the following code to your AppDelegate.swift file.

import UIKit // <- insert below this line
import StitchCore

// set up the Stitch client
let stitch = try! Stitch.initializeAppClient(withClientAppID: Constants.STITCH_APP_ID)

@UIApplicationMain // <- insert above this line
7

Initalize the Remote MongoClient

To checkout the code at this point, enter git checkout step7

You will also add the code needed to Instantiate a RemoteMongoClient object, which provides connectivity support to Atlas.

To do this, call the serviceClient() method on the mongoClient object, passing in the remoteMongoClientFactory and the name of your MongoDB service.

Add the following code to your AppDelegate.swift file.

import UIKit
import StitchCore
import StitchRemoteMongoDBService // <- add this line

// set up the Stitch client
let stitch = try! Stitch.initializeAppClient(withClientAppID: Constants.STITCH_APP_ID)

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?


    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.

        // initialize the RemoteMongoClient
        let MongoClient = try! stitch.serviceClient(fromFactory: remoteMongoClientFactory, withName: Constants.ATLAS_SERVICE_NAME)

        window = UIWindow(frame: UIScreen.main.bounds)
        window?.makeKeyAndVisible()
        window?.rootViewController = UINavigationController(rootViewController: WelcomeViewController())
        return true
    }
    // boilerplate code below
8

Initialize the Remote Mongo Database

To checkout the code at this point, enter git checkout step8

We must also create a RemoteMongoDatabase object to point to the todo database, and then create a RemoteMongoCollection object that points specificallty to the items collection. Your code will then use the RemoteMongoCollection for all CRUD operations against the collection.

import UIKit
import StitchCore
import StitchRemoteMongoDBService

// Stitch initialization
let stitch = try! Stitch.initializeAppClient(withClientAppID: Constants.STITCH_APP_ID)

var itemsCollection: RemoteMongoCollection<TodoItem>! // <- add this line

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        // RemoteMongoClient initialization
        let mongoClient = try! stitch.serviceClient(fromFactory: remoteMongoClientFactory,
                                                    withName: Constants.ATLAS_SERVICE_NAME)

        // add here
        // Set up collection
        itemsCollection = mongoClient
            .db(Constants.TODO_DATABASE)
            .collection(Constants.TODO_ITEMS_COLLECTION, withCollectionType: TodoItem.self)

        window = UIWindow(frame: UIScreen.main.bounds)
        window?.makeKeyAndVisible()
        window?.rootViewController = UINavigationController(rootViewController: WelcomeViewController())

        return true
    }
}

We’ve initialized the collection to be of type TodoItem, which we’ll set up in the next step. We’re no finished with AppDelegate.swift

9

Create the TodoItem Model

To checkout the code at this point, enter git checkout step9

Let’s now create our TodoItem model.

  1. Choose File > New > File in Xcode and select Swift File. Choose Next.
  2. In the Save As dialogue box, name the file TodoItem.swift.
  3. Paste in the following code.
import MongoSwift

// A todo item from a MongoDB document
struct TodoItem: Codable {

    private enum CodingKeys: String, CodingKey {
        case id = "_id"
        case ownerId = "owner_id"
        case task, checked
    }

    let id: ObjectId
    let ownerId: String
    let task: String

    var checked: Bool {
        didSet {
            itemsCollection.updateOne(
                filter: ["_id": id],
                update: ["$set": [CodingKeys.checked.rawValue: checked] as Document],
                options: nil) { _ in

               }
        }
    }
}

We’ve imported MongoSwift to be able to use the ObjectId type. Our TodoItem conforms to the Codable protocol for serialization and deserialization to more easily work with JSON/BSON.

We specify that id should map to _id, and ownerId should map to owner_id. Remember, in MongoDB every document gets an _id field that is unique to that document, and owner_id is the field that we specified in our rules.

The last item of note is the didSet functionality of checked. Whenever this field is modified, it will silently update this for us in the database.

10

Create the WelcomeViewController

To checkout the code at this point, enter git checkout step10

It’s now time to create our WelcomeViewController.

  1. Open ViewController.swift from the project explorer in Xcode.

  2. In the file, highlight ViewController, just before : UIViewController.

  3. In Xcode, choose Editor > Refactor > Rename. Type WelcomeViewController and press the Enter key on your keyboard. This will rename the class and the file.

  4. Paste in the following code. This will add necessary imports and define a viewDidAppear method.

    import UIKit
    import MongoSwift
    import StitchCore
    
    class WelcomeViewController: UIViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
        }
    
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
            title = "Welcome"
            if stitch.auth.isLoggedIn {
                self.navigationController?.pushViewController(TodoTableViewController(), animated: true)
            } else {
                let alertController = UIAlertController(title: "Login to Stitch", message: "Anonymous Login", preferredStyle: .alert)
                alertController.addAction(UIAlertAction(title: "Login", style: .default, handler: { [unowned self] _ -> Void in
                    stitch.auth.login(withCredential: AnonymousCredential()) { [weak self] result in
                        switch result {
                        case .failure(let e):
                            fatalError(e.localizedDescription)
                        case .success:
                            DispatchQueue.main.async {
                                self?.navigationController?.pushViewController(TodoTableViewController(), animated: true)
                            }
                        }
                    }
                }))
                self.present(alertController, animated: true, completion: nil)
            }
        }
    }
    

When this view is presented, we’ll check to see if there is a user already logged in to Stitch. If so, we advance them to the next ViewController, TodoTableViewController which we’ll create in the next step.

If a user isn’t logged in, we’ll create an alert prompting them to log in. For now, this will just use Anonymous login.

If there is an error logging in we use fatalError and log the description of the error. This isn’t ideal but works for the purposes of this small tutorial application.

If the login is successful, we push the TodoTableViewController into our NavigationController, ensuring we do this on the main thread.

11

Create the TodoTableViewController

To checkout the code at this point, enter git checkout step11

We’ll now add our TodoTableViewController. After this step, we will be able to interact with our Todo list, adding and deleting items and marking them as done or not.

  1. In Xcode, choose File > New > File. Choose Cocoa Touch Class, and click Next.
  2. Choose UITableViewController in the dropdown next to Subclass of, and name the Class TodoTableViewController. Do not check the box next to Also create XIB file, and ensure the Language is Swift. Click Next, then Create.

Paste in the following code, replacing the contents of the file.

import MongoSwift
import UIKit

class TodoTableViewController:
UIViewController, UITableViewDataSource, UITableViewDelegate {
    let tableView = UITableView() // Create our tableview

    private var userId: String? {
        return stitch.auth.currentUser?.id
    }

    fileprivate var todoItems = [TodoItem]() // our table view data source

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nil, bundle: nil)
        // check to make sure a user is logged in
        // if they are, load the user's todo items and refresh the tableview
        if stitch.auth.isLoggedIn {
            itemsCollection.find(["owner_id": self.userId!]).toArray { result in
                switch result {
                case .success(let todos):
                    self.todoItems = todos
                    DispatchQueue.main.async {
                        self.tableView.reloadData()
                    }
                case .failure(let e):
                    fatalError(e.localizedDescription)
                }
            }
        } else {
            // no user is logged in, send them back to the welcome view
            self.navigationController?.setViewControllers([WelcomeViewController()], animated: true)
        }
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        title = "Your To-Do List"
        self.tableView.dataSource = self
        self.tableView.delegate = self
        view.addSubview(self.tableView)
        self.tableView.frame = self.view.frame

        let addButton = UIBarButtonItem(barButtonSystemItem: .add,
                                        target: self,
                                        action: #selector(self.addTodoItem(_:)))

        navigationItem.leftBarButtonItem = addButton
    }

    @objc func addTodoItem(_ sender: Any) {
        let alertController = UIAlertController(title: "Add Item", message: nil, preferredStyle: .alert)
        alertController.addTextField { textField in
            textField.placeholder = "ToDo item"
        }
        alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel))
        alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
            if let task = alertController.textFields?.first?.text {
                let todoItem = TodoItem(id: ObjectId(),
                                        ownerId: self.userId!,
                                        task: task,
                                        checked: false)
                // optimistically add the item and reload the data
                self.todoItems.append(todoItem)
                self.tableView.reloadData()
                itemsCollection.insertOne(todoItem) { result in
                    switch result {
                    case .failure(let e):
                        print("error inserting item, \(e.localizedDescription)")
                        // an error occured, so remove the item we just inserted and reload the data again to refresh the ui
                        DispatchQueue.main.async {
                            self.todoItems.removeLast()
                            self.tableView.reloadData()
                        }
                    case .success:
                        // no action necessary
                        print("successfully inserted a document")
                    }
                }
            }
        }))
        self.present(alertController, animated: true)
    }

    func tableView(_ tableView: UITableView,
                   shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {
        return false
    }

    func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        var item = self.todoItems[indexPath.row]
        let title = item.checked ? NSLocalizedString("Undone", comment: "Undone") : NSLocalizedString("Done", comment: "Done")
        let action = UIContextualAction(style: .normal, title: title, handler: { _, _, completionHander in
            item.checked = !item.checked
            self.todoItems[indexPath.row] = item
            DispatchQueue.main.async {
                self.tableView.reloadData()
                completionHander(true)
            }
        })

        action.backgroundColor = item.checked ? .red : .green
        let configuration = UISwipeActionsConfiguration(actions: [action])
        return configuration
    }

    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        guard case .delete = editingStyle else { return }
        let item = todoItems[indexPath.row]
        itemsCollection.deleteOne(["_id": item.id]) { result in
            switch result {
            case .failure(let e):
                print("Error, could not delete: \(e.localizedDescription)")
            case .success:
                self.todoItems.remove(at: indexPath.row)
                DispatchQueue.main.async {
                    self.tableView.reloadData()
                }
            }
        }
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.todoItems.count
    }

    func tableView(_ tableView: UITableView,
                   cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "TodoCell") ?? UITableViewCell(style: .default, reuseIdentifier: "TodoCell")
        cell.selectionStyle = .none
        let item = todoItems[indexPath.row]
        cell.textLabel?.text = item.task
        cell.accessoryType = item.checked ? UITableViewCell.AccessoryType.checkmark : UITableViewCell.AccessoryType.none
        return cell
    }
}

On initializaiton, we check to make sure the user is still logged in. If they are, we query through Stitch to get a list of the user’s Todo items, if empty. We then load these into our todoItems which serves as our datasource, and refresh the TableView.

If a user isn’t logged in, we send them back to the WelcomeViewController.

In viewDidLoad, we set the title of the TableView, and add a button to allow users to create Todo items.

In the addTodoItem function, we present the user with a view so that they can enter a new item. When they press “OK”, we optimistically add the item to our todoList and refresh the TableView. In the callback, we check for a .failure case and in that event remove the item from the todoList and refresh the TableView.

The last few methods provide styling information as well as swipe right left functionality to mark an item as done or not, and to delete the item.

At this point, the application is nearly complete. You can run the application in your simulator, add items, mark them as completed or not, and delete items. Give this a try!

12

Add Logout Functionality

To checkout the code at this point, enter git checkout step12

In this last step, we’ll add the ability to logout of the application.

  1. In TodoTableViewController.swift, add the following code within the viewDidLoad method:

let addButton = UIBarButtonItem(barButtonSystemItem: .add,
                                target: self,
                                action: #selector(self.addTodoItem(_:)))
// add this
let logoutButton = UIBarButtonItem(title: "Logout",
                                   style: .plain,
                                   target: self,
                                   action: #selector(self.logout(_:)))

navigationItem.leftBarButtonItem = addButton
navigationItem.rightBarButtonItem = logoutButton // <- add this

This adds a logout button to the our TableViewController. Let’s implement the logout method now.

  1. In TodoTableViewController, add the following code after the viewDidLoad method:

@objc func logout(_ sender: Any) {
    stitch.auth.logout { result in
        switch result {
        case .failure(let e):
            print("Had an error logging out: \(e)")
        case .success:
            DispatchQueue.main.async {
                self.navigationController?.setViewControllers([WelcomeViewController()], animated: true)
            }
        }
    }
}

This will log the user out, and send them to the WelcomeViewController to log in again.

Note

If you click the logout button and then log in again, all of the todo items entered will be gone. They’re still in MongoDB, but because each anonymous user is given a unique id, they aren’t viewable anymore.

In later stages of this tutorial, we’ll add the ability to sign in with Facebook and Google so that users persist. Stay tuned!

Summary

Congratulations! You now have a working to-do app that lets users anonymously store, view, and complete to-do tasks.

What’s Next

This is the end of the base to-do client tutorial, but there’s still more that we can add. For example, if a user logs out they will not be able to log in with the same anonymous user account and thus will lose all of their saved to-do tasks. To solve this, we can implement a non-anonymous authentication method like email/password or OAuth 2.0 login for Facebook and Google. Look out for the next part of this tutorial, where we’ll integrate non-anonymous authentication into the to-do app.