If you are a regular follower of our blog, you may have heard we released Caravel 1.0.0 few days ago. Otherwise, you are really missing something buddy!

Anyway. I wanted to introduce you to a simple application of Caravel. We are going to build a TODO list using Caravel and Framework7. Framework7 is a web library which incorporates any native UI elements from iOS: lists, interactions, buttons etc. They are all in it! They even released Material Design support a couple of months ago.

In this article, you can find an exhaustive walkthrough for building your amazing TODO list app. However, if you have any comments on it, feel free to post it or email us! We will be more than glad to help you, truly.

Scaffolding

The first step is to create a new app project. For this, open Xcode and select a new Single View Application in the iOS section. We do not need Core Data or unit testing so you can uncheck these options. Name your application as you want. Once initialized, you can upgrade the support to iOS 8.1 and remove the landscape support if you want to (optional).

Is everything good so far? Nice! Simply close Xcode now. In your terminal, navigate to your workspace and run pod init. In the new generated file, add Caravel and RealmSwift as dependencies. Your Podfile should like this:

platform :ios, '8.1'
use_frameworks!

# Target name is your project name
target 'Caravel Todo List' do
  pod 'Caravel'
  pod 'RealmSwift'
end

Alright, install them: pod install is what’re you lookin’ for old chap. Done? Open the new generated workspace then.

Setting up our UI layer

It is time to start building your application now. In your project, create 3 new files: home.html, home.css and home.js. If you create them manually, keep in mind you have to reference them in Xcode (by simply dragging and dropping them in your enclosing project).

Before going further, we have to install some dependencies. Download jQuery and Framework7 (both edge versions). Add jquery.min.js in your project. From the F7 archive, copy framework7.ios.colors.min.css, framework7.ios.min.css and framework7.min.js into your app. Finally, you must reference caravel.min.js from Caravel: you can find it in Pods/Caravel/caravel/js/.

Your project at this point.
Figure: Your project at this point.

Open your home.html file and paste the content below:

<!DOCTYPE html>
<html>
  <head>
    <!-- Required meta tags-->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, minimal-ui">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black">
    <!-- Your app title -->
    <title>Caravel TODO List</title>
    <!-- Path to Framework7 Library CSS, iOS Theme -->
    <link rel="stylesheet" href="framework7.ios.min.css">
    <!-- Path to Framework7 color related styles, iOS Theme -->
    <link rel="stylesheet" href="framework7.ios.colors.min.css">
    <!-- Path to your custom app styles-->
    <link rel="stylesheet" href="home.css">
  </head>
  <body>
    <!-- Views -->
    <div class="views">
      <!-- Your main view, should have "view-main" class -->
      <div class="view view-main">
        <!-- Top Navbar-->
        <div class="navbar">
          <div class="navbar-inner">
            <div class="left"></div>
            <!-- We need cool sliding animation on title element, so we have additional "sliding" class -->
            <div class="center sliding">TODO</div>
            <div class="right">
              <a class="link js-add">Add</a>
            </div>
          </div>
        </div>
        <!-- Pages container, because we use fixed-through navbar and toolbar, it has additional appropriate classes-->
        <div class="pages navbar-through toolbar-through">
          <!-- Page, "data-page" contains page name -->
          <div data-page="home" class="page">
            <!-- Scrollable page content -->

            <div class="page-content">            
                <div class="content-block">
                    <p>Hello World!</p>
                </div>
            </div>

          </div>
        </div>
      </div>
    </div>
    <!-- Path to Framework7 Library JS-->
    <script type="text/javascript" src="caravel.min.js"></script>
    <script type="text/javascript" src="framework7.min.js"></script>
    <script type="text/javascript" src="jquery-1.11.3.min.js"></script>
    <!-- Path to your app js-->
    <script type="text/javascript" src="home.js"></script>
  </body>
</html>

You might have to change the jQuery script path depending on the version you use. Also, copy this piece of code into your home.css:

*:not(input, textarea) {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

Fantastic! We are almost done. To test our setup, we need to edit our storyboard and our Swift ViewController. First open Main.storyboard. In the existing view controller, drag and drop a WebView. Expand it to full the entire space. Finally, add some layout constraints to stick to its wrapper (top, right, bottom and left – using the default values).

Figure: Your storyboard at the end of this milestone.
Figure: Your storyboard at the end of this milestone.

Almost there! Open ViewController. First, create an outlet to your embedded WebView and name it webView. Then, paste this code into viewDidLoad to load the appropriate HTML content:

override func viewDidLoad() {
    super.viewDidLoad()
    webView.loadRequest(NSURLRequest(URL: NSBundle.mainBundle().URLForResource("home", withExtension: "html")!))
    webView.scrollView.bounces = false
    webView.scrollView.contentInset = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)
}

Here we go! Run your app and you should see something like the screenshot below.

Setup screenshot

First iteration

We are going to implement a first version of our TODO list now. For persisting our data, we are going to use Realm. Realm is a great data layer, available for both iOS and Android platforms. You have nothing to setup because you have already installed it when scaffolding the project.

Create a Task class in your project. It represents a task object from our TODO list. It will have 3 fields: an identifier, a label and a boolean denoting if the task has been completed or not.

import RealmSwift

class Task: Object {
    dynamic var id = ""
    dynamic var label = ""
    dynamic var isCompleted = false

    override static func primaryKey() -> String? {
        return "id"
    }
}

To use Realm, your class should match the layout above.

This first iteration includes 3 features: listing tasks, adding tasks and flagging a task as completed. Any interaction with the UI is handled in JS. The JS will then forward any relevant data to our ViewController thanks to Caravel. We are going to build our controller first.

In ViewController, we need first a method for getting a reference to our data layer, used for any reading or writing:

func getRealm() -> Realm {
    var realm: Realm?

    try! realm = Realm()
    // As used on another thread, it is preferable to refresh but not mandatory
    realm!.refresh()

    return realm!
}

We must initialize our bridge connector (aka event bus). First, create a new field for storing the bus in your ViewController class:

// A weak reference is better for preventing any strong cycle reference
private weak var bus: EventBus?

Awesome! Do not forget to import Caravel module first. For setting this field, just add this code to your viewDidLoad method, before any interaction with the UIWebView outlet:

Caravel.get(self, name: "Home", webView: webView, whenReady: { bus in
    self.bus = bus
}

Here, we request a new bus, named “Home”. Our subscriber is our current controller, the watched UIWebView is our outlet and then we would like to run the trailing block on a background thread when the JS bus is ready as well.

Let’s build a method for listing our tasks. Using Caravel, you can only send JSON data. Thus, we must slightly format our data to make them usable by our JS layer.

func listTasks() {
    var outcome: [AnyObject] = []

    let list = getRealm().objects(Task).sorted("label")

    for e in list {
        outcome.append(["id": e.id, "label": e.label, "isCompleted": e.isCompleted])
    }

    self.bus!.post("Tasks", data: outcome)
}

Nice. Now, we must subscribe to 2 events: addition and completion. When adding, we are going to assume the JS module only forwards a label. For completion, it will forward the corresponding id and the new state. Update your whenReady block to this:

Caravel.get(self, name: "Home", webView: webView, whenReady: { bus in
    self.bus = bus

    bus.register("Add") { _, rawLabel in
        let label = rawLabel as! String
        let task = Task()
        let realm = self.getRealm()

        task.id = NSUUID().UUIDString
        task.label = label

        // Any writing has to be done within a write block and with the same Realm instance
        try! realm.write {
            realm.add(task)
        }
        self.listTasks()
    }

    bus.register("Complete") { _, rawData in
        // We have to extract the data from the JSON dictionary
        let data = rawData as! NSDictionary
        let id = data["id"] as! String
        let isCompleted = data["isCompleted"] as! Bool
        let realm = self.getRealm()
        // Assuming the id is correct, otherwise a crash is tolerable
        let task = realm.objects(Task).filter("id = '\(id)'").first!

        try! realm.write {
            task.isCompleted = isCompleted
        }
    }

    // Post tasks as soon as the bus is ready
    self.listTasks()
})

Here, using the returned bus by whenReady, we subscribe to different events by providing the desired event name and a handler. Every handler is called with 2 arguments: the event name and the optional data. To know what kind of data are supported, please refer to the documentation. Also, you receive some “raw data”. You have to cast them to the type you expect before any use. But don’t worry, this cast is safe. Furthermore, in this case, our handlers are run on a background thread. If you need to run some operations that need the main looper (such as any relative to the UI), you should use registerOnMain instead.

Annnnnnd we are done! Our first Swift iteration is ready for shipping. But hold on guys, we have to engineer our JS layer before testing our app.

Firstly, the HTML layout must be improved. Within the page-content div, just paste this code (and remove the Hello World block):

<!-- Task list -->
<div class="list-block js-list-wrapper">
    <ul class="js-list">
        <!-- Item template -->
        <li class="js-task-template">
            <label class="label-checkbox item-content">
                <input type="checkbox" name="tasks">
                <div class="item-media js-toggle">
                    <i class="icon icon-form-checkbox"></i>
                </div>
                <div class="item-inner">
                    <div class="item-title js-label"></div>
                </div>
            </label>
        </li>
    </ul>
</div>

<!-- No content default message -->
<div class="content-block js-no-content">
    <p class="no-content">No task :(</p>
</div>

The different CSS classes and HTML tags we use are matching F7’s specifications. Feel free to skim the manual. Here, this layout includes a task list and a default message to show if there is no task at all. The list also wraps a template. This template is used to render each task from our database.

Our HTML code is ok, let’s write our JS script now. Building the task list is the first step:

// Framework7 initialization
window.framework7 = new Framework7();
window.$$ = Dom7;

$(document).on('ready', function() {
    var taskList = $('.js-list'), listWrapper = $('.js-list-wrapper');
    var noContent = $('.js-no-content');
    var bus = Caravel.get('Home');
    var template = null;
    var checkboxes;

    bus.register("Tasks", function(name, data) {
        if (template == null) { // Extract template
            var t = $('.js-task-template');
            template = t.clone();
            t.remove(); // Remove it from DOM tree
        }

        taskList.empty();
        checkboxes = {};

        if (data.length == 0) { // No task
            listWrapper.hide();
            noContent.show();
        } else {
            noContent.hide();
            listWrapper.show();

            for (var i = 0, s = data.length; i < s; i++) {
                var capture = function(e, t) {
                    var input = t.find('input');
                    input.val(e);
                    input.prop('checked', e.isCompleted);

                    t.find('.js-label').text(e.label);

                    // Store completion state for further update
                    checkboxes[e.id] = e.isCompleted;
                    t.on('click', function() {
                        var v = !checkboxes[e.id];
                        checkboxes[e.id] = v;
                        bus.post('Complete', {id: e.id, isCompleted: v});
                    });

                    taskList.append(t);
                };

                // As we are manipulating loop variables, they need to be captured
                // before being used
                capture(data[i], template.clone());
            }
        }
    });
});

Finally, we need to engineer the addition feature. Enclose this code into the document.onReady listener:

$('.js-add').on('click', function() {
    framework7.prompt(
        "Enter a label",
        "Add a new task",
        function(value) {
            if (value.trim().length > 0) { // Ignore empty values
                bus.post("Add", value);
            }
        },
        function () {} // Ignore
    );
});

Guess what? Time for testiiiiiiing!! You can play with your delicious app :)

Second round

Well, well. Our app is already super awesome. But it could be super duper awesome. I suggest to add these features: pull-to-refresh, edition and deletion. Sounds cool, doesn’t it?

Once again, let’s edit our ViewController. Our Caravel bus needs to handle 3 new events, for the respective features above.

bus.register("Refresh") { _, _ in
    self.listTasks()
}
bus.register("Edit") { _, rawData in
    let data = rawData as! NSDictionary
    let id = data["id"] as! String
    let label = data["label"] as! String
    let realm = self.getRealm()
    let task = realm.objects(Task).filter("id = '\(id)'").first!

    try! realm.write {
        task.label = label
    }

    self.listTasks() // Refresh task list
}

bus.register("Delete") { _, rawId in
    let id = rawId as! String
    let realm = self.getRealm()
    let task = realm.objects(Task).filter("id = '\(id)'").first!

    try! realm.write {
        realm.delete(task)
    }

    self.listTasks()
}

The functions above should be within the whenReady block. That’s all for the Swift backend. We have to edit the front-end part again.

First, update the task template like following. This add some right-swipe actions to each task – in order to allow both edition and deletion.

<!-- Item template -->
<li class="swipeout js-task-template">
  <div class="swipeout-content">
    <label class="label-checkbox item-content">
      <input type="checkbox" name="tasks">
      <div class="item-media js-toggle">
        <i class="icon icon-form-checkbox"></i>
      </div>
      <div class="item-inner">
        <div class="item-title js-label"></div>
      </div>
    </label>
  </div>
  <div class="swipeout-actions-right">
    <a href="#" class="swipeout-close js-edit">Edit</a>
    <a href="#" class="bg-red swipeout-close js-delete">Delete</a>
  </div>
</li>

And here is the associated JS, to insert into the capture local function:

t.find('.js-edit').on('click', function(event) {
    framework7.prompt(
        "Enter a new label",
        "Edit task",
        function(value) {
            if (value.trim().length > 0) {
                bus.post("Edit", { id: e.id, label: value });
            }
        },
        function() {} // Ignore
    );
    // Set input content
    $('.modal-text-input').val(e.label);
    // Close manually swipeout actions
    framework7.swipeoutClose(t);
    event.stopPropagation();
});

t.find('.js-delete').on('click', function(event) {
    framework7.confirm(
        "Are you sure you want to delete " + e.label + "?",
        "Delete task",
        function() {
            bus.post("Delete", e.id);
        },
        function() {} // Ignore
    );
    // Close manually swipeout actions
    framework7.swipeoutClose(t);
    event.stopPropagation();
});

Amazing! Edition and deletion are ready for use. The final step is to support pull and refresh. It’s super easy using Framework7. First, open home.html again and update your page-content block:

<div class="page-content pull-to-refresh-content">
    <!-- Default pull to refresh layer-->
    <div class="pull-to-refresh-layer">
        <div class="preloader"></div>
        <div class="pull-to-refresh-arrow"></div>
    </div>

    <!-- ... Task list below ... -->

Then, open your JS file. Add the following variable declaration at the beginning on the onReady block:

var ptrContent = $$('.pull-to-refresh-content');

ptrContent.on('refresh', function() {
    bus.post('Refresh');
});

This code will raise the Refresh event when the list is pulled. However, using F7, you have to stop the animation when you are done. Add this single code line just before wiping out the task list when receiving Tasks (taskList.empty()):

framework7.pullToRefreshDone();

That’s all folks! You are ready to play with your fast and powerful TODO list!

Epilogue

Hope you liked this tutorial! The full codebase of this app is available here.

Using Caravel, you can easily exchange messages between your JS frontend and your Swift backend. Using this pattern (Swift-Framework7-Caravel) is better than a full JS app. Why? Because, you have a full access to any iOS features (push notifications, Bluetooth, networking, data storage etc.) without any limitation or any extra API. Also, having a Swift backend dramatically improves performance, as JS can be pretty slow. Speaking of that, try to perform as many computations as possible with your backend and not in your JavaScript programs.