February 11, 2016

Building Contactually's iOS App 3.0


A few months ago, our team decided to revisit our mobile experience, and to evaluate alternative means of developing the next version. In the end, we decided to build our app in RubyMotion! We did this for a variety of reasons, but they largely boiled down to familiarity with the language and available libraries (both Ruby and Cocoapods). After a few months of busily writing code, we thought we'd take some time to talk about how that decision worked out.

To start out: we've shipped iOS v3.0! It's in the App Store, and if you haven't already grabbed it, go do that now, it should provide some context to the rest of this article.

Technical highlights of the new app:

    • Customized table views with "cards"

    • Gestures tied to swipe movements on the cards

    • Progressively-loaded lists

    • Detail views with nested tables

    • 400% faster than the previous version (2.4)

    • iPad-friendly through constraints

How We Approached Development

Building an iOS app on a "blank slate" was both a blessing and a curse. We had no technical debts from a previous API or architecture holding us back, but we were also starting from nothing and needed to build it all back up again. We started with the API (v2 will be published soon!), and took an outside-in approach to its development. From there, we built out a Ruby client that allowed us to interact with the API as though it were our persistence layer (database). With the foundation of these models, we were able to concentrate on the views and interactions. Everything else was table stakes. Everyone expects an app to be able to fetch data, update it, and save it for use later. The distinguishing factor between a good and great app is how it feels to the end user.

Stumbling Blocks

While we were very happy with the end result, working with RubyMotion hasn't all been a cakewalk. Being a less-used option, not all libraries were up to date, and not all RubyMotion conventions were the same as our server-side conventions (naming, function-calling style, testing, etc.). In order to bridge some of these gaps, we developed a new testing library on top of RubyMotion's built-in MacBacon clone called motion-spec and contributed to RubyMotion's ActiveSupport clone, motion-support, adding ISO-8601 support to Date/Time. After spending a few weeks to build out these features (and get to know the community via the Motioneers Slack), we had the foundation that we needed to build the app quickly and efficiently, and hopefully enabled those coming after us to do the same.

API-in Development

At the core of our iOS app is the consumption and use of Contactually's API. Because of this, we started the app by implementing ContactuallyApi.client, its known routes, and the classes of object returned from each of those endpoints (e.g. ContactuallyApi::User). In short, we reversed the controller layer of our Rails app.

We organized our network layer with the following structure:

|- errors -- capture HTTP errors
|- models -- Ruby-object representations of resources
|- resources -- modules of methods for HTTP calls

Resource modules were then all included in the ContactuallyApi::Client (which handles indicating network activity, any errors, storage and use of tokens, etc.). Some code to explain it all:

Check out our ContactuallyApi::Contact class as an example:

Converting the JSON and network calls back into standard Ruby objects gave us a great foothold on which to start to interact with those objects without needing to worry about the asyncronous nature of the fetches or the routes they had to occur over.

Getting Started on the GUI

Once we had enough models in place to scaffold out the "look and feel" of the app's core functionality (Prompts, Contacts, Buckets, Notes, and Tasks), we started building out basic table views. We were able to do this relatively quickly thanks to ProMotion, a Ruby gem that wraps standard iOS functions with sane defaults and helper functions that make life much easier. After getting things onto the screen, we started customizing the cells using RMQ (RubyMotionQuery -- a riff on JQuery in name and functionality). RMQ lets you define staticly-sized frames and nest UIViews, UIImageViews, and other iOS objects inside it at relative locations (e.g. upper left is 10 points in and 15 points down). Using these helpers (nicely bundled in the gem redpotion), we were able to get a rough sketch of the app out just before the holidays that looked like this:

Making it Interactive

With the "bones" of our UI in place, we started to turn our attention to the interactions that we expected users to perform. As with the other aspects of the app, we started off with the lowest hanging fruit -- UIButtons that performed an action .on(:tap) { do_something }. This let us test out the "flow" of the application and to make sure that a user could perform all the actions they would need to from a particular screen. For instance, from the Dashboard users would expect to be able to go to the profile of the contact with whom they were being prompted to follow up. They should also be able to perform the action directly there, so wiring up the app to place phone calls, send SMS messages, and compose emails.

Improving the Experience

Once a user was able to do all the things they expected, we started to work on how to enhance the experience with common mobile interactions, like swipe gestures. These were especially important on the Dashboard so that people could come into the app, see the things they were supposed to do for the day, and complete or snooze them quickly. Our goal was to get people into and out of the app as quickly as we could so they could be onto the next part of their day.

We spent a lot of time working on the gestures in the application, refining the distance you have to "pull" the cards, what happens when you do pull them, and how we allow people to revert their decisions. In the end, we tried more than a dozen combinations of distance and action before sending it out to our Beta testers. Then we took their feedback and tweaked it some more. One of the great advantages of developing an app "right" is that you can isolate the functions that are responsible for such interactions and swap them out quickly to find the perfect fit for your app. Here's how we chose to draw that line:

We then included this module in any cells that we wanted to make "swipable", with a couple method overrides (hidden icons or custom panning for resistance), but were able to adjust the thresholds and fading from one place as our users gave us feedback on what worked and what didn't.

Key Takeaways

    1. Plan for the future from the start. Whether it was choosing to go with RubyMotion or how to organize our Models or splitting out components like SwipableCell into Modules, we did our best to take a long view and separate our concerns so that we could change one piece (like update a route URL) and everything else would keep on humming.

    1. Iterate. Throughout the process, we went for the next lowest-hanging fuit and didn't expect perfection from the start. This is core to an "agile" approach, but often this can be lost when it comes to actually building a product. Everyone wants their product to be perfect, but shipped is always better.

    1. Get feedback often. Through each iteration we sent out our app to Beta Testers, both internal and outside of our team. Everyone brings a different perspective and experiences to their view of the product and what it should do, getting lots of eyes on and soliciting what works will emerge trends and even make you think of features you wouldn't otherwise consider.

And now, with charts!

With the launch of our app, and a couple of days of metrics, we're excited to see this take off as people get their hands on the new version, and look forward to seeing these trends grow as we add more features in the coming months.