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
Other Blog Posts
This Is Nowhere: Bloomsday Halifax This Is Nowhere: Why an HTML/JavaScript Single-Page App With GPS Is A Bad Idea This Is Nowhere: GPS and Wayfinding and More UX This Is Nowhere: The Single-Button UX This Is Nowhere: Don’t Just Stand There! This Is Nowhere: Finding My Duck Finding Burgers Fast: My DIY Halifax Burger Week Site "This is Nowhere" at PodCamp Halifax 2018 The Diary Diaries: Fixing Remembary's Facebook Connection Special Leap Day Edition of "Some Weird Things About Time" What's Up With Remembary Can't get pg_dump To Work Now That Heroku Has Upgraded Postgresql to 9.4? The Best Thing I Ever Did To Promote My App If You Build It, They WON'T Come #deployaday, My Big Hairy Plan for 2015 Extracting Plain Text from an NSAttributedString My Year of "Hits" Part 2: Remembary Rolling My Year of "Hits" Part 1: Remembary Rises (and Stumbles) Handy Little Test Method to Check for Translations in Rails Apps My Suddenly Slow-Waking MacBook Air Indie App PR: Keeping Control of Your Tone A Quick Note on 'clone' in Rails 3.2 My eBook Apps 2: iOS, JavaScript, and Ruby My eBook Apps 1: Introduction Quick Tip: No Sound on Mountain Lion My Upcoming Talk at PodcampHFX 2012: My Year of "Hits" starshipsstarthere.ca: Building at the Speed of Funny Screencast Tips Remembary's Cool New Picture Support Indie App PR 2: Keeping On Top Of User Feedback Indie App PR 1: How to Handle an App Disaster Giles Bowkett Diary Project 2 Remembary Video Congratulations! Welcome to Your Nightmare! How My iPad App Remembary Took Off Why You Should Have an App in the App Store (Even If You Probably Won't Make Any Money) PodCampHFX Remembary Presentation - Part 3 How I Used MailChimp Autoresponders to Promote Remembary PodCampHFX Remembary Presentation Part 2 PodCampHFX Remembary Presentation Part 1 Why AdWords Ads Don't Work for iPad Apps Remembary is Sponsoring PodcampHFX Why Can't I Resize my Views in Interface Builder? Momento and Remembary Concerning Remembary iPad-Friendly eBooks of Gracian's Art of Worldly Wisdom Project Report: PTOS2 A Quick Note on Encryption We're all LUsers Thoughts on HAML Friday Afternoon Hack - Getting Beyond the Basics Halifax Friday Hack and Back to Basics Quote from Wil Shipley FutureRuby Make Web Not War Busy Week I: Toronto Ruby Job Fair Employment.nil - the Toronto Ruby Job Fair Code Count: Ruby on Rails vs. C#/ASP.NET A Brief Note on Twitter The Hub Halifax and Mobile Tech for Social Change Deep Thoughts on Microsoft From The Accordion Guy The Two Kinds of Defensive Programming Presentation - Fixing Careerious: From C#/.NET to Ruby on Rails Enterprise! Presenting at Ruby on Rails Project Night - May 7th New Name and New Look for Careerious/Clearfit FutureRuby and More From Unspace Health Tips for Programmers This tables meme won't die Careerious - Ruby and Rails vs. C#/.NET Yeah I Use Tables For Layout, So Sue Me The Different Kinds of Done Giles Bowkett's RubyFringe presentation OfficeTime: Great Time-Tracking App for OS X Back With A New Look Non-DRY Feed torontorb Keeping Your Sanity With The Command Design Pattern shindigital Is All Grown Up! (according to the spambots) Startup Stars? I'm so bored! The Magic Words for RMagick Jennifer from Operations You see? Naming is HARD Business Software as Process Documentation Deployment note: 'execve failed' Steve Jobs on Market Research Why Canada Is Better for Entrepreneurs "Program first and blog second" Toronto Tech Collage The MacBook Air Is A Roadster RubyFringe! Quote of the Week: Steve Yegge Starting Up: Cards Great design tool: browsershots.org Starting Up: The Logo Quotes Of The Day: Hedge Fund Interview TSOT Ruby / Rails Presentation Night - Part 1 Moneyworks: Accounting Software for Canadians on OS X Starting Up: The Name Nice logo, but why is your site so bland? Welcome to shindigital.com