My eBook Apps 2: iOS, JavaScript, and Ruby
Sep 12, 2012 06:21

I recently came out with two iOS-based eBooks, using my own framework: Leadership @ Work and Don't Just Retire!. Here's the story behind the books and the framework.

(By the way these books are half price sale until September 18 2012!)

The previous post talked about the books and the basic ideas behind the framework. This one gets technical and covers not one but three different languages. Hold on to your hats.

Getting and Setting Form Values

It's one thing to show static content in a set of web pages inside UIWebViews in an app. But what about letting the user type things into fields on these web pages? The iOS experience is quite different from the web experience: content should be loaded and saved invisibly and effortlessly behind the scenes, without any user interaction. If you go to a new page, the old page's data should be stored, and the new page's data should already be present. If you have a "SAVE" button in an iOS app, you're doing it wrong.

After a lot of consideration and even more tinkering, I came up with a solution that seemed kind of precarious at first but turned out to run very well:

  • Each field on each form page is given a globally unique id, based on a unique id for the page and a sequential counter. Each field also has a call to a "storeFieldVal()" function in its onChange parameter. This tracks the field's value by the its "id" key in a simple page-global JavaScript key-value store. The fields end up looking something like this:
    <textarea name="B10.3" id="B10.3" onchange="storeFieldVal(this);" </textarea>
  • Now here's the clever bit: iOS's UIWebView has a function called "stringByEvaluatingJavaScriptFromString:" that takes a piece of JavaScript as a parameter, calls it on the current web page, and stores the value. So, on the iOS side, I have a "storeFormValues()" function that calls a series of JavaScript functions on the current page (first one that gets all of the id keys, then a loop of other calls by key) that end up returning all of the field values as NSStrings in Objective-C.
  • The values are then stored in a master data structure, keyed by section, chapter, and question ID.
  • This master data structure is frequently serialized into an XML file in the app's Library directory (a database would have been overkill in this context).
The process works in reverse when loading a page: the data is deserialized out of an XML file, the appropriate chapter answers are found in the data, and a series of "stringByEvaluationJavaScriptFromString" calls are made that send this data to the page, where it's written into the appropriate fields. While the process is fairly simple in theory, there are some tricky bits in the code. The JavaScript has to be able to handle textareas, text fields, checkboxes, and radio buttons in a consistent manner, properly returning the right kind of text. The app needs to be able to get the data from the fields quickly and invisibly, and it has to handle any potential way that the user might leave the current page, including closing the app or turning off the device - and also needs to consider that they might do this while in the middle of editing a field. The Objective-C data layer has to be able to handle dynamic content and serialize/deserialize it properly without any data loss. Thankfully all of the field values are plain text, and even if a user entered huge amounts into every field of every page, the entire data set would probably still be below a megabyte. iOS devices can easily store this amount of data in RAM, and even persistence to a file to flash storage is practically instantaneous.

Building the Content: Ruby to the Rescue

So, I had figured out a way to get data into and out of web pages, but I still needed to assemble dozens if not hundreds of pages of HTML for both the content and the forms. These had to have consistent layout, and the files needed to be organized enough to be easy to manage from within the app. Back in the early 2000s, I built several static but complicated web sites for clients by merging content files into templates. The earliest ones were done using BBEdit's multi-file search and replace tools. Then OS X came out and I discovered the UNIX command line and I built another site entirely using sed - but then I came to my senses and redid it in Perl, which was easier to maintain and ran much faster. Since I'm a Rails developer, I now use Ruby for this kind of thing. While, most of the talk about Ruby in the last five years or so has been about Rails, people often forget how great Ruby is for Perl-like 'glue' tasks such as file munging. I frequently use Ruby when I need to parse or convert big log files, CSVs, or database dumps. Setting up all of these HTML files turned out to be another perfect fit. I set up several template files, with proper HTML head tags, a standard framing layout, and links to the appropriate JavaScript and CSS files. The templates included erb tags for things like the page number, the title, and the page contents:
<h1><%= title %></h1> <%= pagecontents %>
Then I built HTML files for each chapter, with sequentially coded names like 'A01.html', 'A02.html' etc. The files are mostly plain HTML, with some special shortcut tags for things like fields. The key piece of this is a 'data.rb' file, which is basically a large Ruby data structure that describes each of the pages in the book in order with a unique identifier, a listing name, the template type, and other criteria. Here's a sample entry: This is all processed by a 'builder.rb' script. This script does a lot:
  • Goes through each entry in the data.rb file and finds the related source html file.
  • Replaces any standin field tags with real fields, assigning them ids based on the chapter id and a sequential counter, and adding the appropriate JavaScript calls.
  • Merges the parsed content into the appropriate template file.
  • Saves the resulting merged file with the specified name.
  • Adds an entry to an 'AppData.plist' file for the iOS app.

With all these source files and templates and configuration settings in various languages and formats, there's a big risk of things getting confused and messed up. This is solved by having one definitive source: the 'data.rb' file. Everything else comes out of there, generated by the builder.rb script - including the 'AppData.plist' file that is used by the iOS app as the main book reference. This way as long as the data.rb file is correct (and the files it refers to are in the right place) everything else fits together automatically. Whenever I make a change to one of the source files, I just re-run the builder.rb script and all of the other files and configurations are set up automatically.

It may seem weird at first that building and running these eBooks requires three different languages and multiple development and runtime environments - but it actually made everything much easier. This is the reverse of the saying "when you only have a hammer, everything looks like a nail" - complex document layout, interactive forms, and file munging all possible in Objective-C, but they're much easier to do in HTML pages and Ruby scripts. The JavaScript/Objective-C bridge to get data in and out of the pages may seem a bit hairy at first, but it's entirely self-contained and runs cleanly and automatically - each stage of the process only having to deal with some text strings in a data structure, and each context dealing with only what it's best at.

The next post will discuss the business side of these apps.

Previous:
My eBook Apps 1: Introduction
Sep 11, 2012 09:05
Next:
A Quick Note on 'clone' in Rails 3.2
Feb 08, 2013 06:59