"So I got me a pen and a paper and I made up my own little sign. I said thank you Lord for thinking about me, I'm alive and doing fine." - Five Man Electrical Band
Before rambling about my latest tinkerings, I have a small announcement: My wife and I decided to have a baby. She asked me not to make a big deal about it online, not post a broadcast "guess what?!?!" message on Facebook, and in the spirit of that I won't make flowery pronouncements here about the joys of fatherhood, or starting over at 40, or any of the hundreds of other things I would otherwise go on at length about. But there it is: I'm in love with a woman I think the world of, and at the beginning of January, with our third wedding anniversary near us and neither of us having one foot out the door or buyer's remorse, we decided to stop using birth control and try to have a baby.
By the end of the month, Liberty was pregnant. When she was pregnant with Scout, Liberty wanted as natural and non-medicalized birth as possible, preferably a home birth with a midwife to assist. In Ohio, home birth and birthing centers are in a grey area legally. Instead of outright declaring that birth is only allowed in hospitals, the cost of liability insurance for midwifery prices many centers out of existence, and not all medical insurance plans cover them. Liberty unfortunately could not find a local center who took her insurance, but found one in Cincinnati that did. During a 37th week appointment with them, Liberty's blood pressure was elevated, so her midwife recommended going to a hospital for a urine test. At the hospital, the doctors were quick to diagnose preeclampsia, and demanded that she stay and be induced immediately. Needless to say, this wasn't the birth plan she had in mind.
The hospital tried to enforce the standard medical interventions designed to get the doctor to the golf course by tee time: Pitocin, epidural, Cesarean. Her midwife (bless her heart) went to bat for Liberty, telling them to back the hell off, insisting on a private room with a bathtub, that she be allowed to get up and move around, have access to food to keep her strength up, and helped Liberty maintain as much control over the process as possible. Scout's dad was caught between dueling ideologies and unsure of whose side to take. As much as I like him, I don't think he had the life experience needed to cope with a hospital trying to force its agenda on you through scare tactics. He tended to side with the doctors more often than not, but nothing in his experience told him to have anything but faith in what a doctor recommended. Despite that, Liberty's force of will and the strong advocacy of the midwife kept the unnecessary interventions to a minimum.
To be fair to Scout's dad, when Stacey was born, I was of a similar mindset, and Stacey's mom and I thought the hospital was just where you were supposed to go for labor. It wasn't until years later that it dawned on me that doctors are normal people with their own agenda, and a healthy skepticism was needed. I also knew that people still gave birth before there were hospitals, and people were trained to assist the mother before the field of medical obstetrics was invented. It wasn't until recently that it all clicked together for me: In the 60 years that hospital birth in developed countries has been the norm, we've progressed only marginally from twilight sleep and leather straps. The medical community continues to lack empathy and support for mothers in labor, and until this changes, hospital births should be avoided where possible. The field of obstetrics has done too much to disempower and dehumanize women in what is supposed to be an important, memorable, beautiful event. Women should be revered while they're creating life; doing otherwise is God-damned unconscionable.
Back to the story: Because Liberty was induced before her body was ready, her body balked, and she endured a painful 3 day labor. Despite the long labor, the constant onslaught of doctors saying, as if scolding a small child for not cleaning her room, "you know, it's going to be time for a C-section if you don't progress soon," and despite the physical pain of prolonged use of Pitocin, Scout was born vaginally, without need of scalpel or forceps, and from what I've divined in the 3 and a half years I've known her, is both healthy and intelligent. Everything ended well with Scout's birth, but needless to say that isn't the type of birth we want this time.
We found a good midwife center here in Columbus, CHOICE, to see during the pregnancy, and we intend to have the baby on The Farm, a commune that managed to survive after the 70s when the popular hippie movement died down, and home to the mother of modern midwifery, Ina May Gaskin. Ina May authored "Spiritual Midwifery", which I can't recommend highly enough to anyone planning on having a baby, and was interviewed in Ricki Lake's movie, "The Business of Being Born", a must-see for the expecting couple. She is also the namesake of the Gaskin Maneuver, a Guatemalan technique that she popularized that helps with shoulder dystocia - where an infant's shoulder gets stuck when they crown. (When we went down to visit The Farm recently, Liberty saw Ina May driving by, and was all excited. It was cute.)
After being frank about the previous birth to our CHOICE midwife, she said some things that we were happy to hear, namely that preeclampsia is more common in first-time mothers, and a high protein, high calcium diet is good insurance against a repeat occurrence. I found some articles and a study that supported both of those assertions, which was reassuring to me. Before meeting Liberty, I had no experience with what midwives were supposed to be (as is true of most Americans). Based on my experiences so far, I'm sold on them. They are competent professionals, more knowledgeable about childbirth than the surgeons who call themselves obstetricians. Midwives believe, like I do, that childbirth doesn't belong in a hospital with medical intervention, or with the mother on her back, scared, and taking orders.
So coupled with the protein/calcium diet advice, Liberty wanted to monitor her fluid and fiber intake as well, so she set out to look for an iPhone app that could help her track all that. Sadly there weren't any that had all four, or that were customizable. And this is where I come in, using my geek mojo to try to attack the problem.
About 6 months back, I signed up for Apple's developer program, giving them my gmail address, but never heard back. My main home workstations run XP and Linux, so to use XCode to write iPhone apps, I would need to confiscate Liberty's Macbook, giving me two reasons not to code a native app right off the bat. After some digging, I found that Safari supports the offline application cache and web storage specs, and iOS lets you bookmark a website to the "desktop" with a custom icon and splash screen, and provides controls to put Safari in a sort of kiosk mode. In other words, you can click on a button on the main screen, have it open a web app that can save data locally, and the result is basically indistinguishable from a native app. With this approach, I would be able to code for the iPhone with Windows or Linux, and not pay Apple for the right to do so, so that's the road I took.
Getting started
At the beginning of this effort, I decided to code the UI from scratch using un-obfuscated JavaScript to build tables, "buttons" (<span> tags, actually), and onclick handlers. In an attempt to move things along quickly, I didn't fully separate out functionality using MVP, MVC, or other mainstream architectural patterns, but the UI and object functions are fairly split apart. To illustrate, here are functions to build a generic button (UI), and another to remove a food item from a day's eating history (object model).
function makeButton(buttonText, callBack) { var button = document.createElement('span'); button.className = 'button'; button.innerText = buttonText; button.onclick = callBack; return button; } function removeItem(index, dayString) { if (lentil[dayString] == null || lentil[dayString][index] == null) return; lentil[dayString].splice(index, 1); saveData(); }
Whereas, here is sort of a kluge to return a table of a day's eating, that can't decide if it's the user interface, the object model, or a controller:
function getDayTable(dayString) { var outTable = initTable(); if (lentil[dayString] == null) lentil[dayString] = []; var totals = []; for (var n = 0; n < trackElements.length; n++) totals.push(0); var counts = {}; for (var n in lentil[dayString]) { var name = items[lentil[dayString][n]].name; counts[name] = (counts[name] == null) ? 1 : counts[name] + 1; } var seen = {}; for (var n in lentil[dayString]) { var itemNum = lentil[dayString][n]; var item = items[itemNum]; if (seen[item.name] == null) { roundCollection(item.elements); var row = collectionToCountsRow(item.name, item.elements, counts[item.name]); var callback = new callbackRef('verifyRemove', itemNum, n, dayString); row.onclick = callback.invoke; outTable.appendChild(row); incrementTotals(itemNum, totals, counts[item.name]); seen[item.name] = true; } } outTable.appendChild(totalsRow(totals)); return outTable; }
There's still quite a bit of overlap, and as the code progresses I'll get better role separation. Round one, though, quick and dirty, weighing in at only 400 lines of JavaScript and giving my wife something usable while she's still pregnant, a more important concern than glass-tower programming theory.
Lentil
You may have noticed references above to a hash named "lentil". When Liberty was early in her first trimester, she estimated the baby's size to be about that of a lentil. When she would have baby-related symptoms such as an upset stomach, she would say things like "Lentil is really doing a number on me today." So after she balked at my naming the first build of my program "Preggers", she recommended the name Lentil instead, which was more agreeable to both of us.
We shamelessly stole an image off the net for the home screen button:
...but we have a wooden bowl and a bag of lentils at home with which to make a similar image of our own so we won't be dirty cheaters. Or get sued.
Making a web page look like an app
There are a pair of HTML meta tags to enable fullscreen mode and darken the status bar (the top of the iPhone screen that shows your battery level, time, etc.) They are as follows:
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-status-bar-style" content="black" />
Another meta tag can change viewport settings, like disabling zooming, and setting the viewport's width and initial zoom levels:
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
The last iPhone-specific HTML-based hints I used are for setting the home screen icon and the splash-screen. The addresses are relative to the location of the page they are referenced from.
<link rel="apple-touch-icon" href="images/icon.jpg"/> <link rel="apple-touch-startup-image" href="images/startup.png" />
My splashscreen is a little cartoony right now; it's definitely something that will be professionalled-up when this comes out of beta. Here's the current image:
With those HTML tags on a page, you can visit it from Safari on the iPhone, bookmark it to your homescreen, and from then on it will open like a native app. More information about tags that will influence Safari on the iPhone are available here (as well as scores of other pages).
Offline Web applications
The W3C's HTML5 working draft has a nice summary of how offline caching of web applications works, available here. Briefly, the main HTML page references a manifest file. The manifest file lists what files to cache, and the browser caches them. The next time the page is opened, a single query to the web server is made to see if the manifest file has been altered. If it hasn't (or if the browser is offline), all the web resources are loaded from the cache instead of the network.
In other words, if I open a cached web application while the iPhone is in airplane mode, it will still open. Additionally, if the manifest file hasn't been updated, only a single HTTP GET is issued for the manifest file before the application opens, speeding up opening quite a bit.
The caveat is that the manifest file must be served with a content type of "text/cache-manifest" for browsers to turn on caching. In the near future, I imagine by default most web servers will serve .manifest files with this content type, but currently some twiddling is needed - at least, that's true on my current web server of choice, Google's App Engine.
I use Eclipse/Java for deploying App Engine projects, and changing the content type served for a given file extension is accomplished with a few lines in the project's web.xml file:
<mime-mapping> <extension>manifest</extension> <mime-type>text/cache-manifest</mime-type> </mime-mapping>
To verify this was working as expected, I deployed a project, telnetted to the web server on port 80, and did a manual GET for the file:
GET /lentil.manifest HTTP/1.1 Host: lentiltracker.appspot.com HTTP/1.1 200 OK ETag: "kRe4nQ" Date: Sat, 02 Apr 2011 08:47:45 GMT Expires: Sat, 02 Apr 2011 08:57:45 GMT Cache-Control: public, max-age=600 Content-Type: text/cache-manifest Server: Google Frontend Transfer-Encoding: chunked a3 CACHE MANIFEST # Beta 9. # js files json2.js lentil.js ...rest of file 0
As you can see, the content type came through correctly. Rather than the browser downloading the entire file each time and comparing it with a cached copy, the HTTP entity tag reference (ETag) is used to determine if the web server thinks the file has changed. This is accomplished by including an "If-None-Match" field in the GET request, specifying the last ETag value. If the browser gets back an HTTP 304 response, it knows there's no need to re-download the entire file. Here's how that looks over telnet:
GET /lentil.manifest HTTP/1.1 Host: lentiltracker.appspot.com If-None-Match: "kRe4nQ" HTTP/1.1 304 Not Modified ETag: "kRe4nQ" Date: Sat, 02 Apr 2011 08:53:31 GMT Server: Google Frontend
My project has a number of resource files with it: The main HTML page, a CSS file, JavaScript file, the icon and splashscreen images, and of course the manifest file. My App Engine resource usage will look something like this for requests that come after all of those files have been cached:
Ideally, just the manifest file, and most of those should be HTTP 304s. What I've found in practice, though, is that App Engine gives the occasional HTTP 200 for a file that hasn't changed, which leads me to believe they don't always store the ETag field the same way on all mirrored servers, or possibly each App instance regenerates new values. Consequently, Safari busies itself with comparing the file contents, and sees that all is well so doesn't re-download the CSS, JavaScript or main page files. In some of these cases, the iPhone's desktop subsystem will re-download the icon and splashscreen files, sending a user agent containing CFNetwork and Darwin, contrasted with Safari's AppleWebKit. A typical set of log entries looks like this:
Even considering that the 304s aren't consistent, this is still a lot less traffic than having each file be downloaded fresh each time the app runs. The web traffic benefit is good reason to use application caching of static content (or code) where possible, even if you aren't trying to make a web page look like a native iPhone app.
Since Safari on the iPhone and Google Chrome both use the WebKit engine, I did most of my testing for this project with Chrome on Windows for the basic reason that its debugging tools are fantastic. For example, from the chrome://net-internals/ URL, you can capture and view raw network bytes, as well as see when Chrome is grabbing a resource from the application cache instead of the network. For example, here is how Chrome responded to the order to load my app's CSS file:
Start Time: Sat Apr 02 2011 04:33:06 GMT-0400 (Eastern Daylight Time) t=1301733186906 [st= 0] +REQUEST_ALIVE [dt=69] t=1301733186906 [st= 0] +URL_REQUEST_START_JOB [dt=29] --> load_flags = 65536 (VERIFY_EV_CERT) --> method = "GET" --> priority = 1 --> url = "http://lentiltracker.appspot.com/lentil.css" t=1301733186916 [st=10] APPCACHE_DELIVERING_CACHED_RESPONSE t=1301733186935 [st=29] -URL_REQUEST_START_JOB t=1301733186975 [st=69] -REQUEST_ALIVE
In fact, the net-internals page is where I first noticed the business with ETags and HTTP 304 responses:
If you want to see all the stored manifest files, or delete individual application caches, you can accomplish that from chrome://appcache-internals/, as shown in this screenshot:
Storing data locally
In the past, having a browser store data locally could only be accomplished with HTTP cookies, the much-maligned bastard stepchild of the Internet. There are two major drawbacks to using them, namely size limits (4k per cookie, 20 cookies per domain), and the fact that they are sent by the client with every request. The original Netscape recommendation for cookies indicated the idea was for them to manage "state". The server would be notified what state the browser thought it was in, and the server could make decisions based on that.
A newer recommendation for web storage is being hashed out for the HTML5 standard, and Chrome and Safari (and I assume other browsers) already have it implemented, complete with a handy JavaScript accessor: localStorage.[name] = [string] for storing simple strings. There is also a web database recommendation that allows local DB objects that can be queried with SQL, but I chose the simple localStorage method so I could get something functional quickly, and used JSON to turn my JavaScript objects into strings.
I ended up with a pair of simple functions that manage turning an eating history object and a food types object into localStorage strings, and back again:
var lentil = {}; // History hash var items = []; // Array of food types function init() { if (localStorage == null) { alert("Local storage features won't work in this browser."); } else { if (localStorage.lentilHistory) lentil = JSON.parse(localStorage.lentilHistory); if (localStorage.lentilItems) items = JSON.parse(localStorage.lentilItems); } } function saveData() { localStorage.lentilHistory = JSON.stringify(lentil); localStorage.lentilItems = JSON.stringify(items); }
Chrome's developer tools can be used to see the raw contents of the local storage DOM tree, as shown here:
Now that I have my feet under me with web apps, I can either databasify this, or at least split foods and days into unique objects so that the entire history can't be blown away with one bad write. How to manage doing that and save all the data Liberty already has entered will take some hemming and hawing, but shouldn't be too much of a puzzler.
The rest is all JavaScript to handle events and turn arrays of food items into tables. Here are a few screenshots of how the pages look now, but it needs a little usability tweaking, and a lot of prettying up.
More tweaks of the app will come, and in all likelihood I'll leave it up on the App Engine page as a free tool. The end result that I care about, though, is Liberty's use of it. She is tracking her calcium and protein, and can see at a glance if she's on the road to avoiding a second hospital birth. As a husband who cares about this sort of thing, I'm doing everything in my power to support a natural birth. I'm just glad in this case there is enough overlap between what I'm good at and what Liberty needs so that I can contribute.
Wow I didn't know the pregnancy was planned...so happy for the two of u I know the new addition to the family will bring a lot of happiness to everyone. Love ya!
ReplyDelete