We recently released AnyList for the Web, which allows AnyList Complete subscribers to view and edit their lists in a web browser on their Mac or PC.
Creating a web version of AnyList is something we’ve wanted to do for a while, as it has been one of our top feature requests. However, we were concerned about the amount of time it would take to rewrite AnyList for a new platform as well as the ongoing effort required to maintain it. As a team of just two people, we must be vigilant about taking on too much at once and spreading ourselves too thin.
Luckily, we found some relatively new technologies that let us get the initial version of AnyList for the Web up and running quickly and that should also minimize the amount of time we need to spend keeping it up to date with the iOS app.
oj
We implemented the web app’s model and sync code using oj, an Objective-C inspired superset of JavaScript that was created by our friend Ricci Adams for his musictheory.net website.
Using oj allowed us to write object-oriented JavaScript with Objective-C syntax, including the use of some modern Objective-C features such as @property and @synthesize. The end result is that many of our oj classes are line-for-line equivalent to the corresponding files in our iOS app.
To get an idea of how similar our oj code looks to Objective-C, take a look at this sample from our NavigationController class written in oj. (Note: the @synthesize
is only necessary because we wanted to prefix our instance variables with ‘m’ to match the AnyList iOS app.)
@implementation NavigationController : ViewController {
id mToolbar;
}
@property Array viewControllers;
@synthesize viewControllers = mViewControllers;
@property id delegate;
@synthesize delegate = mDelegate;
- (id) initWithRootViewController:(ViewController)rootViewController {
self = [super init];
if (self) {
mViewControllers = [rootViewController];
[self addChildViewController:rootViewController];
[rootViewController didMoveToParentViewController:self];
}
return self;
}
- (void) pushViewController:(ViewController)viewController animated:(Boolean)animated {
var topViewController = [self topViewController];
var delegate = [self delegate];
if ([delegate respondsToSelector:@selector(navigationController:willShowViewController:animated:)]) {
[delegate navigationController:self willShowViewController:viewController animated:animated];
}
[self addChildViewController:viewController];
[viewController didMoveToParentViewController:self];
mViewControllers.push(viewController);
[self _didShowViewController:viewController animated:animated wasPushed:YES];
}
@end
When Ricci first told us about oj, we were admittedly a bit hesitant about using something that needed to be compiled to JavaScript (at the time, we had been looking into either writing everything ourself or using an MVC JavaScript framework such as AngularJS, Backbone.js, or Ember.js). However, after experimenting with oj, we quickly realized that we could have large parts of the code for the web app be nearly identical to the code in the iOS app, and the implications of that were hard to ignore.
First, translating our Objective-C code to oj was very straightforward, often involving straight copy/paste and making a few minor tweaks such as replacing NSArrays with native JavaScript arrays. Having such similar code between the two platforms will also be a big win as we continue to maintain the web app. When we make a change in the iOS app, we know that we need to make the same change to the web app and vice versa. In fact, just through the process of porting the iOS code over to the web app, I uncovered a few bugs and ported fixes back to the iOS app.
The biggest benefit of using oj and having such similar code on both platforms is that the web app is now built on code that is already well tested and that we are intimately familiar with. As anyone who has been following Brent Simmons and his series of Vesper Sync Diary posts knows, designing a sync system is an extremely challenging problem. With oj, instead of re-writing our sync code to work within a new framework and having potentially subtle implementation differences between the web app and iOS app, we’re essentially running the same syncing code that has been powering the AnyList iOS app for the last two years.
React
AnyList’s iOS interface is largely composed of UITableViewController subclasses and custom UITableViewCells. For the web, we followed this same basic structure and implemented our own ViewController, TableViewController, and NavigationController classes in oj.
To implement our view objects (e.g. versions of UITableView and UITableViewCell), we used React, a JavaScript library from Facebook/Instagram for creating reusable UI components. When writing our React components, we used JSX, a JavaScript XML syntax transform. This allowed us to use HTML-like snippets inside our React component render methods, which makes for very clean and concise code. This is what the render method for our split view looks like using JSX:
var ALSplitView = React.createClass({
render: function() {
return (
<div className="ALSplitView">
<div className="ALSplitView-MasterContainer">
{this.props.masterViewBuilder.build()}
</div>
<div className="ALSplitView-Separator" />
<div className="ALSplitView-DetailContainer">
{this.props.detailViewBuilder.build()}
</div>
</div>
);
}
});
Each oj ViewController has a React component associated with it (just like a UIViewController has a UIView associated with it). When a ViewController receives a notification from the model that something has changed, the ViewController simply tells its React component to re-render. Internally, React uses a virtual DOM and with each rendering pass determines what changes need to be made to the real DOM (it does this really, really quickly).
Once we had our base ViewController classes in oj and our UI components in React, we were able to pretty quickly port all of our UITableViewController subclasses over to oj. Often, the oj class needed even less code than its iOS counterpart. For example, the oj TableViewController subclasses did not need a heightForRowAtIndexPath: method.
After the core interface was implemented, we were also able to add some basic animations to the app by taking advantage of React’s CSS animation hooks. For example, we added a sliding animation when pushing/popping views in our navigation controller classes as well as when presenting a modal view controller.
Conclusion
The combination of oj and React has worked out really well for us. We were able to make the initial version of AnyList for the Web available to our users with just a few months of work and since the code structure is so similar to the iOS app, we expect maintaining it and keeping it up to date with the iOS app to be a relatively easy and straightforward process.
Want to be informed when a new post is available? Sign up to be notified via email. Infrequent updates, no spam: