Main

September 30, 2009

My Deck for 360idev

Thanks to all who attended my talk at 360idev. I think it went rather well and I am grateful to all who attended; the audience makes the talk. Here are the slides and the sample code as promised. Please read the readme file.

I'll also take some time on the plane home tomorrow night to make versions of this sample code without blocks for those who like to kick it old school. I'll post those here as soon as I can.

UPDATE: I've changed the last example to remove the blocks requirement for those who wish to avoid them. Same download location here as before.

Note: Be aware that the slides are on a pretty slow internet connection.

February 24, 2009

Useful bash foo with subversion

I've been a command-line user of subversion (svn) for some time and have long enjoyed these little bits of bash foo.

Stripping .svn directories

I often start one-off little test projects in my private slush svn repository and then move them into their own repository for further development as (or if) they grow up. Sometimes you'll want to get rid of svn's footprints in a working directory.
rm -rfv `find . -name *\.svn`
The find . -name *\.svn finds all of the .svn folders that svn uses to track the repository and with the help of those handy back-tics, rm -rv does the recursive removal of svn tracking directories.

Adding new files

During that early "making lots of new stuff" phase, I often generate quite a number of files that need to be added to svn at once. This is one of those times where having (say) class files in their own subdirectory is nice. Running this command will svn add every file that isn't currently being tracked by svn in the current directory.
svn add `svn stat |grep \? |awk '{print $2}'`
Grep finds every svn stat output line that includes a question mark (meaning the item is not currently under version control) and pipes it through awk '{print $2}', which shows only the text (the filename) from the second column of the svn stat output. The back-tics and svn add finish the magic.

Using a Mulligan

I'll use something similar to the above along with svn revert to rollback a working copy "all the way." Say you're experimenting in your working directory; you've added a few files, hit a dead end, and want to revert the whole tree as well as delete anything you've added. In other words, you're looking for a full-on "do over." Using svn revert --recursive will revert any files that svn is tracking, but will leave anything svn is not tracking alone, so we have to also remove those files.
svn revert --recursive && rm -rf `svn stat |grep \? |awk '{print $2}'`

As with any other nerd foo, make sure that you really want to do what you're asking for when you use these commands. These make my life with svn just a little easier.

January 03, 2009

iPhone responsiveness and memory usage

I recently answered a question on a private mailing list about how to make a network - based (XML parsing and such) iPhone application more responsive. I've been encouraged to post it here by a few folks (Thanks, guys! You know who you are.). So, I figure "why not?" Here you go. (Slightly modified)

The original question (paraphrased): I have an iPhone application that downloads XML data from the web, parses it, and loads it all into memory. I notice that my app is a little sluggish and that it crashes as odd times, probably due to having too much information in memory. There are other apps that do this kind of thing on the app store and I notice that many of them are more responsive than mine. How does one make one's "network cloud-based" app more responsive?

My Answer

You're running into a few of the more common problems with embedded programming. Here are a few little tricks that have helped me in general (done a little embedded Linux programming, too) and on the iPhone.

Regarding bandwith / data size / responsiveness as a result of downloading data

You'll be surprised that the actual downloading of anything but pretty large amounts of data is pretty fast by itself. The part that is slow is the stuff that happens before the download even starts. Edge / 3G latency sucks and even DNS lookup is really really really (really?) slow on the iPhone in almost all circumstances AFAICT. I've found that strategies that download more data at a time while discarding unneeded data that you're getting on the fly, rather than making multiple requests for specific small chunks of data has been a performance win. That's a little counter intuitive.

Regarding memory

There is a reason the NSXMLDocument doesn't exist on the iPhone. You can get away with throwing a smallish XML tree into memory and go ahead and do it if it simplifies your life for small data sets, but the combination of larger XML trees in memory and whatever else (including UI) you have cached in memory can cause you to hit the memory ceiling at weird times; that's probably what's happening when your app crashes. The app will get terminated if you don't release memory when you get these memory warnings. Take a look at apples "Books" SQLite example. They're not doing exactly what you're looking for there (no network code), but the handling of memory works as they're doing it. They load data from a SQLite database lazily, but keep that data in memory until the application closes, until the owning object is deallocated, or when memory gets scarce. When the app gets those fun memory warnings, they save any changed data and release everything that they can get back out of the database later. This is similar in theory to the lazy loading of views we've all grown accustomed to (and that is partly handled for you). When a view controller gets a memory warning, its views are released by default if they don't have a superview, which is why you have to figure out if the view is still there every time you (re)load it. Doing something similar with your XML data by saving it somewhere when you get a memory warning and releasing the associated data structure, reloading it piecemeal from disk as needed (be it an SQLite database or what-have-you) works pretty well and appears to be the recommended approach. Ultimately, figuring out how much and which data to put on "disk" and/or in memory (and when) is the biggest PITA in embedded programming, but it's also the place where you're going to get the responsiveness you're looking for. http://furbo.org/2008/08/27/dealing-with-memory-loss-the-cleanup/

Regarding UI responsiveness

Don't use a lot of subviews and avoid opacity. Instead draw the views yourself... Draw once, if possible. ( http://blog.atebits.com/2008/12/fast-scrolling-in-tweetie-with-uitableview/ )

Other tricks

Use Instruments and Shark with the code running ON THE DEVICE to see how much memory you're using, how much disk access you're doing, and what is actually going on timing-wise. Optimize those specific areas profiling the simulator (faster edit, compile, test cycle). Rinse (on the device) and repeat.

Put breakpoints in memory warning methods and figure out when (and why) it's happening (again, on the device). Is it when you download data, load view A, then view B? Can you release view A's data when it goes off the screen to keep the memory footprint from growing? Do you need the whole tree for view B? Can you download it all, dump to disk, and load lazily? Blah blah blah. This stuff can get tricky, but try the obvious things first. Find out what is causing the memory ceiling problem and you're well on your way. :)

Using the LLVM/Clang static analyzer has saved me a lot of headaches as well. It's easy to forget that you have a web view somewhere with a bunch of cached data floating off screen causing the device to go bork bork...

Cache yesterday's data, load, and display it before going out to the network for today's data. Use a progress indicator and some other UI indicator to let the user know that you're updating from the network and that they're looking at what may be old data. Do this asynchronously. When it comes to displaying data from the network, sometimes you have to fake it 'til you make it. You don't have to rewrite Google gears or anything, just throw everything you need from the server onto disk and reload it when the app launches.

Gus Mueller's FMDB rocks for SQLite work. I have a version with embedded Dtrace probes if anybody wants it. (This will have to be the subject of a later article after I find some time to update my version to Gus' latest version and to send it to him to look at / integrate if he wishes first...)

November 15, 2008

touchengine -- iPhone Google App Engine communication

My friend Noah Gift, an awesome python programmer who wrote the very popular book Python for Unix and Linux System Administration and I are working on a new open source framework that aims to facilitate communication between the iPhone SDK and Google App engine. In the spirit of the open source mantra, "release early, release often," we've made it available here on Google Code and is MIT licensed. We're also working on a couple of apps that use the framework; we plan to eat our own dogfood so-to-speak.

Current features

  • Includes a slightly modified version of the python plist library to allow syndication of data from Google App Engine to the iPhone via xml plists.
  • Includes a generically useful caching plist downloader library for the iPhone SDK that keeps the user in sync with Google App Engine data and allows offline access to that data.

Example Code

  • isonnet, a Google App Engine application that syndicates Shakespeare's Sonnets in plist form for consumption by the iPhone app.
  • Sonnet, a viewer application for the iPhone, which we're going to release soon for free on the app store, that connects to our Google App Engine site to download, cache, and display Shakespeare's Sonnets.

Future Features / Informal Roadmap

  • Authentication with Google App Engine with the user's Google ID
  • Two-way communication and data sync between app engine and the iPhone SDK
  • Integration and automatic plist syndication of underlying Google App Engine Data Storage and objects
  • Support for Application skinning through plist syndication
  • Support for storage of the iPhone user's application preferences on Google App Engine.
Look for our article on IBM DeveloperWorks coming soon.

If you have any questions or comments for us, please feel free to contact me at jonathan ((at)) thisdomainyou'reonrightnow ((dot com)).

November 14, 2008

Reverse DNS on the iPhone

Reverse DNS via the usual OS X means doesn't seem to work on the iPhone. It looks like this is a known bug/limitation. The new apple developer forums (login required) have a thread that's dedicated to the problem. (rdar://problem/5929766 is mentioned there). Apple seems to have used the eraser on some other of their DNS code on the iPhone. Saurik has an interesting hack to work - around a perhaps related change in DNS behavior.

You can actually get reverse DNS lookup on the iPhone to work using res_query (which is in libresolv, so make sure you link against libresolv.dylib) to query DNS and dns_parse_packet, which you'll find in dns_utils.h, to do the work of parsing out the DNS server reply works rather well. I could not get the recommended tools in dns.h to work, but I did discover that res_query returns the same raw reply from the DNS Server that dns_parse_packet expects from the utilities in dns.h. The code:

Continue reading "Reverse DNS on the iPhone" »

October 01, 2008

iPhone NDA -- some thoughts

When the news came out today that Apple has chosen to lift the iPhone NDA, I (like many developers) breathed a sigh of relief. I would like to take a moment to thank my peers for being so outspoken and to thank Apple for finally deciding to make my life as an iPhone developer excedingly easier by allowing the members of the development community to do as they so often and so unselfishly do to help one another with various recurring difficulties that crop up in our craft. The Mac OS X software community that I've had the pleasure of getting to know over the last couple of years continues to astound me. I'm proud to count myself among them. They are, to a person, a wonderful, well-meaning, honest, and interesting group of people who, as evidenced by the recent brouhaha over the aforementioned NDA, are very vocal about their beliefs and try to treat others as they expect to be treated. I've decided to take a page from that book and voice a belief of mine: A contract is a contract.

Having had the pleasure of arguing over contracts (including several of my own), intellectual property, personal liberty, and many similar topics with more than a few software developers, attorneys, students, musicians, my astonishingly intelligent parents, and various other people I have admired over the years, I've come to a few conclusions. One of the fundamental principals I try to follow in my own dealings with others is, I'm told, a basic brocard of civil law -- Pacta sunt servanda ("agreements must be kept").

Continue reading "iPhone NDA -- some thoughts" »

September 14, 2008

Using Shark and custom DTrace probes to debug Nagios on Mac OS X

Nagios has for several years represented a favorite wrench in my networking toolbox. It does a fantastic job of monitoring various hosts and services, warning the sysadmin if things start to get wonky, usually well before any user notices a change in service. It compiles and runs cleanly and has always performed like a champ for me. I wouldn't want to run a server without it.

I recently purchased a Mac Mini to use as a small portable network monitor for those occasions when I require some short-term network monitoring. While I strongly considered installing Linux on it, as that's what I usually use for this sort of thing, I decided to build the tools I needed in OS X Leopard. I use Leopard Server at work (I love the newiCal server), and since Leopard is officially Unix compliant, I didn't expect too much trouble, especially with great sources of FOSS for the Mac like MacPorts.

I installed Nagios through Macports (sudo port install Nagios), configured a few hosts to check, and tested the warning system. Everything was happily up and running and I had moved on to downloading and installing ntop and a few other tools when I noticed the CPU fan spooling up. Running top indicated that nagios was spinning a whole processor core. Hmm.

Google tells that others have seen this problem, but I could find no solution online. Let's find out why this is happening.

Continue reading "Using Shark and custom DTrace probes to debug Nagios on Mac OS X" »

December 09, 2007

Always - on Print Preview

Some software is, in my experience, not quite WYSIWYG when it comes to the final printed output so I have gotten into the habit of viewing a PDF in preview before I actually send a document to my printer. This has saved many trees.

Apple makes it fairly easy to do this. Just command-P and click on the weird-looking PDF button to get the drop down menu to select "Open PDF In Preview."

preview.jpg

I would like to see most every print job automatically open in preview without needing to take my hands off of the keyboard. One can make this happen with a little bit of effort, some help from Folder Actions, and a little piece of free third-party software. Here's how I do it.

First, download and install Cups PDF per the instructions on its home page. This will create a printer and driver that dumps a pdf file into ~/Desktop/cups-pdf/. Set your default printer to CUPS-PDF.

Next, you will setup a folder action on the ~/Desktop/cups-pdf/ to open each file that gets dumped in there in preview. I keep my folder actions scripts in ~/Desktop/Folder\ Actions/, so I put the following applescript (derived from a similar script that ships with Leopard) there.

  
(*
add - new item alert

This Folder Action handler is triggered whenever items are added to the attached folder.
The script will display an alert containing the number of items added and offering the user
the option to reveal the added items in Finder.

Copyright © 2002–2007 Apple Inc.

You may incorporate this Apple sample code into your program(s) without
restriction. This Apple sample code has been provided "AS IS" and the
responsibility for its operation is yours. You are not permitted to
redistribute this Apple sample code as "Apple sample code" after having
made changes. If you're going to redistribute the code, we require
that you make it clear that the code was descended from Apple sample
code, but that you've made changes.
*)

property dialog_timeout : 5 -- set the amount of time before dialogs auto-answer.

on adding folder items to this_folder after receiving added_items
try
tell application "Finder"
--get the name of the folder
set the folder_name to the name of this_folder
end tell

-- find out how many new items have been placed in the folder
set the item_count to the number of items in the added_items
--create the alert string
set alert_message to ("Folder Actions Alert:" & return & return) as Unicode text
if the item_count is greater than 1 then
set alert_message to alert_message & (the item_count as text) & " new items have "
else
set alert_message to alert_message & "One new item has "
end if
set alert_message to alert_message & "been placed in folder " & «data utxt201C» & the folder_name & «data utxt201D» & "."
set the alert_message to (the alert_message & return & return & "Would you like to view the added items?")

display dialog the alert_message buttons {"Yes", "No"} default button 1 with icon 1 giving up after dialog_timeout
set the user_choice to the button returned of the result

if user_choice is "Yes" then
tell application "Preview"
--fire it up
activate
--open the items
open the added_items
end tell
end if
end try
end adding folder items to

Ctrl (or right) click the cups-pdf folder, Enable Folder Actions, then Configure Folder Actions via the contextual menues seen below.


EnableFolderActions.jpg

When you select "Configure Folder Actions" you'll set the script that runs every time a new file is added to that folder to the one above.

folderActions.jpg


Now every time you hit command - P to print in a application, you'll hit enter to print to your default CUPS-PDF printer, which will write a PDF to the cups-pdf folder on your desktop. The folder action will then pop-up this window (for five seconds).

PreviewMe.jpg


All you have to do is hit enter.

So! Every time I print I hit command - P, enter, wait a sec, enter. Instant preview.

December 03, 2007

Poking around in others' software is sometimes useful (and thanks to Zorn)

I need an NSNumberFormatter subclass for a PyObjC project I'm working on that reformats an NSNumber to hours:minutes:seconds. Thinking I've seen this before (and kind of hoping that there was some voodoo I was missing somewhere to make this simple), I decided to poke around in applications that deal with time. After a little head-scratching, I was reminded of that most useful time-tracking and invoicing application I've grown to love called Billable (Zorn!). On this screenshot (from the Clickable Bliss site) we see a field labeled "Time Spent:" with a "Start" button next to it. Thinking to myself, "I want that formatter!" I fired up F-Script Anywhere, injected it into Billable and dug down until I was the class name for the formatter. CBTimeLengthFormatter.jpg Time to pray to google. Ah! Zorn! You beautiful helpful coding-type person. You've pasted it for us. Thank you! Now all that is left is to pythonify it. (I made a few modifications to the behavior, but it's the same general idea)
#
#  CBTimeLengthFormatter.py
#  PuppyTracker
#
#  Created by Jonathan Saggau on 12/3/07.
#  Copyright (c) 2007 __MyCompanyName__. All rights reserved.


from Foundation import *
from math import floor

#modified from http://paste.lisp.org/display/21854
class CBTimeLengthFormatter(NSNumberFormatter):

    def stringForObjectValue_(self, anObject):
        if (not (anObject.isKindOfClass_(NSNumber))):
            return(None)
            
        if (anObject.intValue() <= 0):
            return("00:00:00")
        intval = int(floor(anObject))
        
        hours = intval / (60*60)
        minutes = (intval - (hours * 60 * 60)) / 60
        seconds = intval - (minutes * 60) - (hours * 60 * 60)
        string = "%02i:%02i:%02i" %((hours), (minutes), (seconds))
        return(string)
    
    def getObjectValue_forString_errorDescription_(self, objVal, inString, err):
        """Take a string like "00:00:00" and turns it into a NSNumber and returns YES
           Also able to handle 10:10 (as 10 minutes, 10 seconds) and 10 (as 10 seconds)"""
        
        string = NSString.stringWithString_(inString)
        
        #catch for nil or empty string
        if (string == None or string.isEqualToString_("")):
            return True, 0, None
        
        stringList = string.split(":")
        #make seconds first, instead of hours
        stringList.reverse()
        
        #turn each into an integer, filtering out empty strings
        try:
            stringList = [int(each) for each in stringList if each is not u'']
            
        #if we can't make any part of this into an int, bail
        except ValueError, e:
            return False, 0, None
        
        #make sure we have Seconds, Hours, Minutes by padding the list with zeros
        #in case we have (say) Seconds, Hours only
        while ( len(stringList) < 3):
            stringList.append(0)
        
                        #sec            #min               #hour
        timeInSeconds = stringList[0] + stringList[1]*60 + stringList[2]*60*60
        return True, timeInSeconds, None

November 24, 2007

Living with multiple Macs

I have three machines running os X:
  • My Macbook Pro for travelin' to them clients; it lives wherever I am.
  • My iMac for writing code on a big screen; it lives on my desk
  • My Mini for watching movies, listening to music, serving web pages, serving mail, serving my calendars; it lives in my Middle Atlantic rack (highly recommended rack manufacturer, by the way. Nice stuff.)
Synchronizing my data (especially my home directly) has recently become much more important to me. I've been spending rather more time physically at clients and can't afford the "I think I edited that on the desktop machine, which is turned off at home" problem. For code, this is handled quite nicely by subversion, but I also need a simple way to synchronize my home directories between my laptop and my desktop. (I don't so much care about the contents of my home directory in my server because I don't really produce content there). My friend Noah Gift, who really is a gift to my world would tell you to use nfs and apple's offline synchronization to do this for you. This is fancy and fine voodoo, and something I used to do way back when I used Linux primarily (gasp) but I didn't really want to serve my home directory from a central location. Version controlled home is also overkill for me and dealing with conflicting files in svn can be a bit of a pain (yes, Hal, I thought about your suggestion as well). I really want my laptop to act kind of like my iPhone. I want to come home, plug in, deal with a few conflicts if and only if I care at that point, and sync without having to also think.
What is need is a simple directory synchronizer. Ok. Shouldn't be hard to find, right? Wrong. Every (payware, even) GUI file synchronizer I've tried has ended in the spinning beachball of death. Grumble Grumble. Ok again. Time to slide off into command-line utilities. I thought maybe I would use git instead of svn, but I don't want to use git (yet). I'm resistant to use anything designed for version control for this purpose and I'm too lazy to learn yet another one, even though git is supposed to be very fast at resolving differences between trees. Maybe later. Sorry Linus.
Enter a little-known synchronizing tool called Unison. I Love this software.
From the Unison site:
  • Unison runs on both Windows and many flavors of Unix (Solaris, Linux, OS X, etc.) systems. Moreover, Unison works across platforms, allowing you to synchronize a Windows laptop with a Unix server, for example.
  • Unlike simple mirroring or backup utilities, Unison can deal with updates to both replicas of a distributed directory structure. Updates that do not conflict are propagated automatically. Conflicting updates are detected and displayed.
  • Unlike a distributed filesystem, Unison is a user-level program: there is no need to modify the kernel or to have superuser privileges on either host.
  • Unison works between any pair of machines connected to the internet, communicating over either a direct socket link or tunneling over an encrypted ssh connection. It is careful with network bandwidth, and runs well over slow links such as PPP connections. Transfers of small updates to large files are optimized using a compression protocol similar to rsync.
  • Unison is resilient to failure. It is careful to leave the replicas and its own private structures in a sensible state at all times, even in case of abnormal termination or communication failures.
  • Unison has a clear and precise specification.
  • Unison is free; full source code is available under the GNU Public License.
In other words, exactly what I need. :) Good instructions for setting it up for this purpose are available on the Linux Journal site. It's easy to keep it from synchronizing certain directories (I leave ~/Library and ~/svnCheckouts alone, for example) and it's easy to keep it from synchronizing certain file types as well (.DS_Store, .Spotlight*, .Trashes, etc.). If I reorganize my entire home folder on the train, that reorganization is mirrored on the desktop. No muss. No fuss. No "svn move."
So! When I come home, I shutdown my laptop (it's usually sleeping in my bag), restart holding the T key, which turns it into a very expensive external drive, connect a firewire 800 cable between it and my desktop and synchronize. Happy me.

No more Finder, no more Dock

I don't use the either anymore.
I find the Terminal sufficient for most large-scale file movings (especially with the help of tree). Path Finder is where I live to browse my filesystem. Launching applications (also searching the web and making coffee) belongs to Quicksilver, which was recently open-sourced!
FYI: tree compiles on OSX Leopard if you make the following very small changes to tree.c:
--- ./tree-1.5.1.1/tree.c
+++ ./tree-1.5.1.1JS/tree.c
@@ -17,7 +17,7 @@
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

-#include 
+//#include 
#include 
#include 
#include 
@@ -182,7 +182,7 @@
#ifdef CYGWIN
extern int MB_CUR_MAX;
#else
-extern size_t MB_CUR_MAX;
+extern int __mb_cur_max;
#endif

int main(int argc, char **argv)

November 11, 2007

More Leopard calDAV fun. Migration!

As you know from my last post, I have lots and lots of calendars. It used to be much much worse. Sharing calendars between my three Macs before Leopard server was a pain in the ass. Sure, you could put a calendar on a regular DAV server and subscribe to that calendar on your other machines, but you could not write to that calendar from any machine other than that which created it initially. All subscribed machines were read-only. What does this mean to me? You know about my 14 calendars, right? Triple it. That's right. I used to have 42 calendars on each of my 3 machines. 14 calendars to which I could write and 28 to which I was subscribed so I could read any appointments from my other two machines. What a mess.

Leopard calendar server has brought me back to a debatably sane 14 calendars. But how does one migrate all of those calendars spread across multiple machines onto the shiny new calDAV server? Like so:

1. Subscribe to your shiny new calDAV server (under the Accounts tab) in iCal preferences.
2. Create a new calendar on your calDAV server through iCal (File Menu -> new Calendar -> your server) for each of your 14 calendars.
3. Create a *.ics file from your old calendar by:
a. Selecting the calendars you want to export by using the export functionality in iCal (File Menu -> Export) on each of your machines or...
b. Mounting your DAV server and downloading all of 'em at once (assuming you're keeping your calendars on a DAV server now, which I was, this method is a lot easier for obvious reasons)...
4. Select the calendar from the source list (one residing on your new calDAV server) and import the *.ics files you wish to store in the new calendar (File Menu -> import).

Your appointments are now on your calDAV server and will show up on all of your subscribed machines fully editable from all.

November 06, 2007

Leopard server, calDAV, and Mozilla Sunbird

For those wishing to use Mozilla Sunbird with the calDAV server that comes with Leopard, it's not setup in Sunbird in (quite) the same way as it is in iCal.

Let's say you have the calDAV Account URL working in ical (which will subscribe you to all of your calendars) and its URL is

 https://somedomain.com:8443/principals/users/username/ 

In Sunbird, you'll have to subscribe to the your calendars individually. Every calDAV user I've setup so far in Leopard server has been given a default calendar called simply "calendar", which you can subscribed to from Sunbird at

https://somedomain.com:8443/calendars/users/username/calendar/
Easy enough. However, if you add a new calendar in iCal, it will create a calendar named with a UUID. In order to figure out what that UUID is, it's easiest to just mount the calDAV in the finder (Command - K) at
https://somedomain.com:8443/calendars/users/username/
You'll see several directories in there: calendar, dropbox, oubox, notifications, etc. The one that looks like a UUID (for example, 037D3206-9C1D-4C6C-8E54-B3E3CAF90ABF) is the directory you'll want to subscribe to in Sunbird. In the example above, you would subscribe to
https://somedomain.com:8443/calendars/users/username/037D3206-9C1D-4C6C-8E54-B3E3CAF90ABF/

October 26, 2007

Yeah, it breathes

Wow. Alex.SpeechVoice is creepy. Apple's latest "read your email to you" speech synthesizer breathes (!) before it (he?) starts speaking. It's also, by far, the largest file in my installation of Leopard.

See?

LepprSpace.jpg

Update:

The software generating the view above is called GrandPerspective and I love it. Thanks to DeRay for emailing me about this.

September 04, 2007

MarsEdit 2 is suhweet

Having been a MarsEdit user since before its Red-Sweater-ization, of course I just bought me an upgrade for that there brand spankin' new MarsEdit 2.0. It's awesome, as expected.

Daniel Rocks.

Carry on, punkass.

August 14, 2007

iPhone Native Pong Application: the real "sweet" SDK

The native iPhone hack I put together for the C4[1] conference is in the process of a "wow, I hope somebody can read this code someday" cleanup. I will release the source in the coming days. Stay tuned for a download link.

In the meantime, here are a couple of teasers.

Screenshots:

scrnsht1.jpg


scrnsht2.jpg


And here is some code for playing a sound file from your native iPhone Application's bundle (with the class-dump'd headers you'll need, too). The iPhone, as was mentioned by everyone with a native hack at C4, has some really intelligent API. Hats off, ladies and gentlemen, to the iPhone team. They must be having a grand ol' time internally playing with this thing.



//
// PongAudio.h
//
// Created by Jonathan Saggau on 2007-08-12.
// Copyright (c) Jonathan Saggau All rights reserved.
//

#import <Foundation/Foundation.h>
@class AVItem;
@class AVController;
@class AVQueue;

@interface PongAudio : NSObject
{
AVItem *bounce;
AVItem *loser;
AVQueue *q;
AVController *controller;
}

-(void)playBounce;
-(void)playLoser;

-(void)stop;
@end

//
// PongAudio.m
//
// Created by Jonathan Saggau on 2007-08-12.
// Copyright (c) 2007 __MyCompanyName__. All rights reserved.
//

#import "PongAudio.h"
#import "AVItem.h"
#import "AVController.h"
#import "AVQueue.h"

//From Aaron hillegass
#define LogMethod() NSLog(@"-[%@ %s]", self, _cmd)

@interface PongAudio (PrivateAPI)
-(void)play:(AVItem *)item;
@end

@implementation PongAudio


- (id)init
{
self = [super init];
if (nil!= self)
{
NSError *err;
NSString *path = [[NSBundle mainBundle] pathForResource:@"PongBounce" ofType:@"wav" inDirectory:@"/"];
bounce = [[AVItem alloc] initWithPath:path error:&err];
if (nil != err)
{
NSLog(@"err! = %@ \n item = [[AVItem alloc] initWithPath:path error:&err];", err);
exit(1);
}

path = [[NSBundle mainBundle] pathForResource:@"PongLose" ofType:@"wav" inDirectory:@"/"];
loser = [[AVItem alloc] initWithPath:path error:&err];
if (nil != err)
{
NSLog(@"err! = %@ \n item = [[AVItem alloc] initWithPath:path error:&err];", err);
exit(1);
}

controller = [[AVController alloc] init];
[controller setDelegate:self];
q = [[AVQueue alloc] init];
[q appendItem:bounce error:&err];
if (nil != err)
{
NSLog(@"err! = %@ \n [q appendItem:item error:&err];", err);
exit(1);
}

[q appendItem:loser error:&err];
if (nil != err)
{
NSLog(@"err! = %@ \n [q appendItem:item error:&err];", err);
exit(1);
}
}
return self;
}

- (void)dealloc
{
[bounce release]; bounce = nil;
[loser release]; loser = nil;
[q release]; q = nil;
[controller release]; controller = nil;
[super dealloc];
}

-(void)playBounce
{
[self play:bounce];
}

-(void)playLoser
{
[self play:loser];
}

-(void)play:(AVItem *)item;
{
[controller setCurrentItem:item];

//play NOW
[controller setCurrentTime:(double)0.0];
//should probably check this eventually, too.
//- (BOOL)isCurrentItemReady;
NSError *err;
[controller play:&err];
if(nil != err)
{
NSLog(@"err! = %@ [controller play:&err];", err);
exit(1);
}
}

-(void)stop;
{
[controller pause];
}

- (void)queueItemWasAdded:(id)fp8
{
LogMethod();
}
@end


//AVController.h
/*
* Generated by class-dump 3.1.1.
*
* class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2006 by Steve Nygard.
*/


#import <Foundation/Foundation.h>
#import "CDStructures.h"

@interface AVController : NSObject
{
struct AVControllerPrivate *_priv;
}

+ (void)setEnableNetworkMode:(BOOL)fp8;
+ (id)avController;
+ (id)avControllerWithQueue:(id)fp8 error:(id *)fp12;
- (id)initWithError:(id *)fp8;
- (void)setAVItemClass:(Class)fp8;
- (id)init;
- (void)dealloc;
- (struct AVControllerPrivate *)privateStorage;
- (BOOL)isNewImageAvailableForTime:(const CDAnonymousStruct1 *)fp8 willNeverBeAvailable:(char *)fp12;
- (long)copyImageForTime:(struct __CVBuffer **)fp8 time:(const CDAnonymousStruct1 *)fp12;
- (void)itemPropertyDidChangeNotification:(id)fp8;
- (void)scheduleQueueSpaceCheck;
- (void)checkQueueSpace;
- (void)queueItemWasAdded:(id)fp8;
- (void)queueItemWasAddedNotification:(id)fp8;
- (void)queueItemWillBeRemovedNotification:(id)fp8;
- (void)cancelPrepareForPlayback;
- (void)setQueue:(id)fp8;
- (id)queue;
- (void)feederRangeWasInserted:(id)fp8;
- (void)feederRangeWasRemoved:(id)fp8;
- (void)feederInvalidatedWithCurrentItemMoved:(id)fp8;
- (void)setQueueFeeder:(id)fp8 withIndex:(int)fp12;
- (void)setQueueFeeder:(id)fp8;
- (id)queueFeeder;
- (BOOL)setRepeatMode:(int)fp8;
- (int)repeatMode;
- (BOOL)havePlayedCurrentItem;
- (id)currentItem;
- (id)currentItemPriv;
- (void)setCurrentItem:(id)fp8 preservingRate:(BOOL)fp12;
- (void)setCurrentItem:(id)fp8;
- (void)makeCurrentItemReady;
- (BOOL)isCurrentItemReady;
- (void)continueAfterRepeatGap;
- (void)cancelContinueAfterRepeatGap;
- (void)doCancelContinueAfterRepeatGap;
- (void)doScheduleContinueAfterRepeatGap:(id)fp8;
- (BOOL)beginRepeatGap;
- (void)itemFailedPlaying;
- (void)itemFinishedPlaying:(id)fp8;
- (void)itemCompletedDecode;
- (BOOL)play:(id *)fp8;
- (void)pause;
- (void)dequeueFirstItem;
- (unsigned int)indexOfCurrentQueueFeederItem;
- (BOOL)setIndexOfCurrentQueueFeederItem:(unsigned int)fp8 error:(id *)fp12;
- (id)addNextFeederItemToQueue;
- (BOOL)playNextItem:(id *)fp8;
- (float)rate;
- (BOOL)shouldBeginPlayingItem:(id)fp8 error:(id *)fp12;
- (BOOL)setRate:(float)fp8 error:(id *)fp12;
- (BOOL)resumePlayback:(double)fp8 error:(id *)fp16;
- (id)errorWithDescription:(id)fp8 code:(long)fp12;
- (void)makeError:(id *)fp8 withDescription:(id)fp12 code:(long)fp16;
- (BOOL)beginInterruption:(id *)fp8;
- (BOOL)activate:(id *)fp8;
- (void)endInterruptionWithStatus:(id)fp8;
- (float)volume;
- (void)setVolume:(float)fp8;
- (double)currentTime;
- (void)setCurrentTime:(double)fp8;
- (BOOL)muted;
- (void)setMuted:(BOOL)fp8;
- (void)setEQPreset:(int)fp8;
- (int)eqPreset;
- (struct OpaqueFigVisualContext *)visualContext;
- (void)setVisualContext:(struct OpaqueFigVisualContext *)fp8;
- (id)outputQTESFilePath;
- (void)setOutputQTESFilePath:(id)fp8;
- (id)lkLayer;
- (void)setLayer:(id)fp8;
- (id)attributeForKey:(id)fp8;
- (BOOL)setAttribute:(id)fp8 forKey:(id)fp12 error:(id *)fp16;
- (struct _LKImageQueue *)lkImageQueue;
- (struct _LKImageQueue *)lkEnsureQueueForWidth:(unsigned int)fp8 Height:(unsigned int)fp12;
- (double)lkServerTime;
- (BOOL)okToNotifyFromThisThread;
- (void)fmpTimeJumped;
- (void)fmpRateDidChange;
- (void)rateDidChangeToRate:(float)fp8;
- (void)repeatModeHasChanged:(int)fp8;
- (void)currentItemWillChangeToItem:(id)fp8 oldItemCurrentTime:(double)fp12;
- (void)currentItemHasChanged:(id)fp8;
- (void)itemHasFinishedPlayingNotification:(id)fp8;
- (void)resynchronizeTiming;
- (id)delegate;
- (void)setDelegate:(id)fp8;
- (BOOL)setItemAttribute:(id)fp8 value:(id)fp12 forKey:(id)fp16 error:(id *)fp20;
- (id)itemAttribute:(id)fp8 forKey:(id)fp12;
- (id)initWithQueue:(id)fp8 error:(id *)fp12;
- (BOOL)isValid;
- (id)initWithQueue:(id)fp8 fmpType:(unsigned long)fp12 error:(id *)fp16;
- (void)applyAttributesFromItem:(id)fp8;
- (void)fmpRelease:(id)fp8;
- (void)failPlayback:(id)fp8 reason:(long)fp12 notifyClient:(unsigned char)fp16;
- (void)prepareForPlaybackReply:(long)fp8;
- (int)instantiateFMPRef:(struct opaqueFigMoviePlaybackRef **)fp8 forItem:(id)fp12;
- (void)maybeDumpPerformanceDictionary:(struct opaqueFigMoviePlaybackRef *)fp8;
- (void)removeFMPRefListeners:(struct opaqueFigMoviePlaybackRef *)fp8;
- (void)shutdownFMPRef:(struct opaqueFigMoviePlaybackRef *)fp8;
- (void)updateTimeMarkerObservations;
- (void)scheduleUpdateTimeMarkerObservations;
- (void)registerTimeMarkerObserver:(id)fp8 forItem:(id)fp12 times:(id)fp16 context:(id)fp20;
- (void)removeObserver:(id)fp8 fromTMOArray:(id)fp12;
- (void)unregisterTimeMarkerObserver:(id)fp8 forItem:(id)fp12;

@end

//AVExternalAudio.h
/*
* Generated by class-dump 3.1.1.
*
* class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2006 by Steve Nygard.
*/

#import <Foundation/Foundation.h>
#import "CDStructures.h"

@interface AVExternalAudio : NSObject
{
struct AVExternalAudioPrivate *_priv;
}

+ (id)avExternalAudio:(id)fp8;
- (id)initWithDelegate:(id)fp8;
- (void)dealloc;
- (id)attributeForKey:(id)fp8;
- (BOOL)setAttribute:(id)fp8 forKey:(id)fp12 error:(id *)fp16;
- (void)makeError:(id *)fp8 withDescription:(id)fp12 code:(long)fp16;
- (void)postServerConnectionDiedNotification:(id)fp8;
- (void)fmpServerConnectionDied;
- (BOOL)okToNotifyFromThisThread;
- (BOOL)activate:(id *)fp8;
- (float)volume;
- (BOOL)isActive;
- (void)postUserVolumeChangedNotification:(id)fp8;
- (void)fmpUserVolumeDidChange;
- (void)fmpChangeConnectionActive:(BOOL)fp8;

@end

//AVItem.h
/*
* Generated by class-dump 3.1.1.
*
* class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2006 by Steve Nygard.
*/

#import <Foundation/Foundation.h>
#import "CDStructures.h"

@interface AVItem : NSObject
{
struct AVItemPrivate *_priv;
}

+ (id)avItem;
+ (id)avItemWithPath:(id)fp8 error:(id *)fp12;
- (id)initWithError:(id *)fp8;
- (id)init;
- (id)initWithPath:(id)fp8 error:(id *)fp12;
- (void)dealloc;
- (BOOL)setPath:(id)fp8 error:(id *)fp12;
- (int)_instantiateItem;
- (id)path;
- (double)duration;
- (struct CGSize)naturalSize;
- (float)volume;
- (void)setVolume:(float)fp8;
- (void)setEQPreset:(int)fp8;
- (int)eqPreset;
- (id)attributeForKey:(id)fp8;
- (BOOL)setAttribute:(id)fp8 forKey:(id)fp12 error:(id *)fp16;

@end

//AVQueue.h
/*
* Generated by class-dump 3.1.1.
*
* class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2006 by Steve Nygard.
*/

#import <Foundation/Foundation.h>

@class NSMutableArray, NSRecursiveLock;

@interface AVQueue : NSObject
{
NSRecursiveLock *_mutex;
NSMutableArray *_items;
void *_reserved;
unsigned int _flags;
}

+ (id)avQueue;
+ (id)queueWithArray:(id)fp8 error:(id *)fp12;
- (id)initWithError:(id *)fp8;
- (id)init;
- (id)initWithArray:(id)fp8 error:(id *)fp12;
- (void)dealloc;
- (int)_instantiateItem;
- (unsigned int)itemCount;
- (id)itemAtIndex:(unsigned int)fp8;
- (unsigned int)indexOfItem:(id)fp8;
- (id)itemAfterItem:(id)fp8;
- (BOOL)appendItemsFromArray:(id)fp8 error:(id *)fp12;
- (BOOL)appendItem:(id)fp8 error:(id *)fp12;
- (void)itemWasAdded:(id)fp8;
- (BOOL)insertItem:(id)fp8 atIndex:(unsigned int)fp12 error:(id *)fp16;
- (BOOL)insertItem:(id)fp8 afterItem:(id)fp12 error:(id *)fp16;
- (void)itemWillBeRemoved:(id)fp8;
- (BOOL)removeItem:(id)fp8;
- (BOOL)removeItemAtIndex:(unsigned int)fp8;
- (void)removeItemsInRange:(struct _NSRange)fp8;
- (void)removeAllItems;

@end

July 01, 2007

My other iphone is a rental.

This post may be short, given that I'm learning to type with this thing. I do have to make a little confession. This is my backup iPhone. That's right. Thinking a client (who, I've learned, got his before me) would want one, and realizing I could return a sealed product, I grabbed two when I went to the Apple store. With the number transfer and activation problems, I made an impulsive decision to pay the restocking fee on this one and unboxed iPhone number 2. Now I think ATT got suspicious of me trying to activate two phones on two plans... Well, let's just say that the prepaid plans are no great deal. (yes, they accepted my credit the first time around). Oh well. My other iPhone is a rental.

March 07, 2007

Webkit and cocoa bindings.

During the C4 conference, I remember Brent Simmons (NewsGator, NetNewswire) and others talking about various interesting possible ways of using a web view in cocoa. I remember thinking about how cool it might be to build some custom UI in a web browser window using html, css, and javascript instead of using drawInRect and friends in my custom views. Wouldn't it be terribly cool if cocoa bindings would be easy to connect our cocoa data model to various bits of (say) an html form. This would allow us to sucker our web designer friends into making us a pretty skin for our app. Slick. (Not that this idea is so new... reference the iTunes store).

Jim Speth, the guy who inadvertently taught me PyObjc with his PyInjector project, has been working on a bit of code on top of WebKit that accomplishes the above by adding cocoa bindings to the DOM of an embedded web view. Originally written in PyObjC (woot!) it's a framework containing a subclass of NSTreeController and a custom WebView to allow us direct, Cocoa bindings aware, hierarchical access to the DOM. Cool stuff. Recently, he's been invited to add this functionality to WebKit itself. Good on ya, Jim! This functionality is now available in the current nightly builds of WebKit. Jim has also been kind enough to have recently posted an example app that shows us how to access and modify the width and height of images in a webview through cocoa bindings and some javascript. It requires that you download and mount the nightly dmg of WebKit to run. It's very pretty. Download it! You get to stretch Jim's face.

In my own app I would like to present the user with a fairly simple form-based web interface for entering data that I (or perhaps the power user) can then skin with custom css to allow easy export to a regular web site or blog. All of the data will live in a coredata store and I would really like the web view to seamlessly integrate with the rest of cocoa. I really don't want the app to look like a glorified web browser. With this in mind, I twisted Jim's example around to connect a slider and an NSTextField to a text input field in a WebView. It's available in my subversion repository at http://jonathansaggau.com/svn/JavaScriptBindings and is mostly Jim Speth's code.
Jim's code includes an Objective C class that fires KVC/KVO notifications in Cocoa for a corresponding javascript function. It allows us to call JSKVC.willChange(document, 'boxInfo'); from our javascript to fire the Cocoa KVC methods, which in turn allow us to use Cocoa bindings to connect everything together.

@implementation JSKVC
+ (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector
{
    if (aSelector == @selector(object:willChangeValueForKey:))
        return NO;
    if (aSelector == @selector(object:didChangeValueForKey:))
        return NO;
    return YES;
}
+ (NSString *)webScriptNameForSelector:(SEL)aSelector
{
    if (aSelector == @selector(object:willChangeValueForKey:))
        return @"willChange";
    if (aSelector == @selector(object:didChangeValueForKey:))
        return @"didChange";
    return nil;
}
- (void)object:(id)object willChangeValueForKey:(NSString *)key
{
    [object willChangeValueForKey:key];
}
- (void)object:(id)object didChangeValueForKey:(NSString *)key
{
    [object didChangeValueForKey:key];
}

The HTML is just a common text input box that we give a unique ID so that javascript can identify it.
<html>
<head>
<title>bindings example</title>
<script type="text/javascript" language="javascript" src="boxinfo.js"></script>
</head>
<body>
    <p><input type="text" id="box" value="0" maxlength="4" name="contentBox" /></p>
</body>
</html>
</pre>

We need a little javascript magic now to model our text box and connect things up to the KVC notifier class. First we build a simple prototype class to define accessors for the text box value.

// bindings.js BoxInfo.prototype = { get content() { if (!this.box) return null; return this.box.value; }, set content(w) { if (!this.box) return; this.box.value = w; }, }
Then we'll need a method that will call our cocoa KVO willChange and didChange methods. JSKVC will get injected into the javascript runtime from cocoa below.
function BoxInfo() {
    this.box = null;
    this.setBox = function(newBox) {
        // store the new box value, with KVO notifcations
        JSKVC.willChange(document, 'boxInfo');
        this.box = newBox;
        JSKVC.didChange(document, 'boxInfo');
    }
}
To finish things off in javascript, we need to tell it which box to deal with. Jim's code has a nice bit of functionality that allows the user to choose what he/she is modifying by clicking on an image. Mine is simply getting the box we're interested in through its unique ID.
function installBoxInfo() {
    // create an BoxInfo object for the (DOM) document, using KVO notifications
    JSKVC.willChange(document, 'boxInfo');
    document.boxInfo = new BoxInfo();
    JSKVC.didChange(document, 'boxInfo');
    
    var boxElement = document.getElementById( 'box' );
    document.boxInfo.setBox(boxElement);
    //boxElement.setAttribute('style', 'outline: -3px solid white;');
    boxElement.onblur = function() { 
        JSKVC.willChange(document, 'boxInfo');
        JSKVC.didChange(document, 'boxInfo');
    };
}
The boxElement.onblur event (for those of us, like me who aren't familiar with javascript) is the event that gets triggered when the user leaves the text box. Since the user is likely to have changed the contents of the text box, we call willChange and didChange to let cocoa know it needs to update the other widgets and doodads. More on this in a bit.

Now we need to get all of this into the cocoa runtime. In the WebFrameLoadDelegate of the WebView, we need to inject some javascript code. This is where things get really interesting. We're evaluating our boxinfo.js file and injecting the JSKVC class into javascript so that we can fire KVC notifications in Cocoa.
- (void)webView:(WebView *)sender windowScriptObjectAvailable:(WebScriptObject *)windowScriptObject
{
    // as soon as the window object is available, inject the code for the boxInfo object
    NSString *stringForBoxInfo = [self stringForResource:@"boxinfo" ofType:@"js"];
    [windowScriptObject evaluateWebScript:stringForBoxInfo];
    // add the key-value notification object to the script environment
    [windowScriptObject setValue:[[JSKVC alloc] init] forKey:@"JSKVC"];
}
Then, once the web frame is loaded, we need to setup our BoxInfo object by calling the installBoxInfo(); function
- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame
{
    // modify the DOM document each time a new page loads
    [[webView windowScriptObject] evaluateWebScript:@"installBoxInfo();"];
}
And, should we ever want to (say) call a method in javascript from cocoa, we set up a WebScriptObject by evaluating a small bit of javascript code corresponding to the object we want to message (document.boxInfo below) and then call- (id)callWebScriptMethod:(NSString *)name withArguments:(NSArray *)args;
WebScriptObject *boxInfo = [[webView windowScriptObject] evaluateWebScript:@"document.boxInfo"];
[boxInfo callWebScriptMethod:@"setBox" withArguments:[NSArray arrayWithObject:obj]];
That's about all of the code. Now let's take a look at the NIB file.

The interesting bits in the window below will be the webview, the NSTextField, and the slider. The other views are bound to various parts of the DOM.

webWindow.jpg

There is a DomTreeController from Jim and the WebKit folks and a simple NSObjectController to hold the boxInfo class (from javascript)

WebJavaNibClasses.jpg

The DomTreeController is bound to a new attribute of the WebView called mainFrameDocument. This gives us access to the whole of the web page DOM through cocoa bindings.

bindingJavsc1.jpg

The NSObjectController is bound to the mainFrameDocument.boxInfo object that we defined in javascript. (Which would be document.boxInfo in JS)

bindingJavsc2.jpg

All the is left is to bind both of the cocoa controls (NSTextView and the NSSlider) to the boxInfo NSObjectController (boxInfo.selection.content) and, well, Bob's your uncle (hi Jo).

bindingJavsc3.jpg

Yo run the code we need to have the nightly build of WebKit mounted on our machine. We can set the following environment variables on our executable in xcode and run from there or we can just dump them in a script and allow that script to run the project. We're telling dyld to look for frameworks in the WebKit.app (more on this in a later post) and to unset the system WebKit framework path (causing dlyd to ignore the older version that's installed on our machine).
#!/bin/tcsh
set PATH=`dirname $0`
setenv DYLD_FRAMEWORK_PATH /Volumes/WebKit/WebKit.app/Contents/Resources/
setenv WebKit_UNSET_DYLD_FRAMEWORK_PATH YES
$PATH/build/debug/JavaScriptBindings.app/Contents/MacOS/JavaScriptBindings

And now for my favorite screenshot.

JavaScriptBoundAndGagged.jpg

Thanks to Jim's magic WebKit additions and cocoa bindings. The value in the NSTextField, the NSSlider and in the text input field of the WebView are all synchronized. We can even use the web page to input data.

In a couple of days, I'll revisit this topic for a short post on how to build WebKit as an embeddable framework so you can use this now in a shipping application (at 24MB, it's pretty big) and how the nightly builds of WebKit work. Stay tuned.

February 27, 2007

Core Data Double Linked List How To

I have been working on a core data app lately and stumbled upon some data that should maintain relative order. Since core data is a collection of objects stored in a database of sorts, this is not supported out of the box. There are a couple of ways to accomplish this.

Most of us who find ourselves in this situation will keep an incremental index as an instance variable in each member of our collection and sort based on that index number. Under certain circumstances, this is the best way to do it. However, A kind of nasty problem arises when we want to reorder the collection of objects. Either we're forced to auto-increment the indices by something other than 1 to allow us a little wiggle room for local insertion or reordering of objects or we're going to have to re-index most (or all) of the objects in our collection any time the order changes or an object is inserted. For instance, if I were to insert an object at the front of an indexed list, I would probably have to iterate through the entire collection to increment the index number. No fun.

While reading the long back and forth on the cocoadev list from June of 2005, I stumbled upon a post by Bill Bumgarner that I found quite interesting.

Actually, depending on context of implementation, using a linked list 
is both quite easy and very efficient.  Obviously, inserting/deleting 
objects into a linked list is trivial and fast if you have both a 
next and a previous pointer.  Given that Core Data will maintain the 
inverse relationship automatically, it will also maintain the linked 
list pointers automatically.


Given that many UI type usage contexts will require all of the objects to be faulted into memory anyway, walking the "next" relations and filling an NSMutableArray for display purposes is about three lines of code.

... In this particular example, the amount of overhead in the form of additional lines of code incurred by the developer is pretty trivial. ... b.bum
He's writing about a common data structure called a double (or doubly) linked list wherein each object in the list holds a pointer to the next and previous objects that can, in certain situations, be more useful for ordering data than an indexed set. We can enumerate in either direction very easily, asking each object in the chain for next or previous until we get nil and we can insert, remove and reorder objects with little hassle. All we have to do is make sure that next and previous values of adjacent objects are set appropriately (more on this in a second). Unfortunately, We can't (efficiently) get an object at an index like we can with an indexed list because we're purposely not indexing the collection. Also, remember that much of cocoa will want a data structure that responds to objectAtIndex which, as Bill mentiones above, will require us to iterate the chain and put it into an array. We also can't place an object in the chain at two different positions (but two objects with identical attributes can coexist in the chain).

To use core data for ordered collections we have a decision to make. Either we can make insertion and reordering fast or we can make getting an object at an index fast. If you like the indexed set idea you can go here:

Jesse Grosjean's implementation and Uli Kusterer's as well

If you like the double linked list idea, stick around. You can download the whole package from my svn repository by getting subversion and typing the following into the terminal:
svn co https://jonathansaggau.com/svn/MOLinkedList
Data Model

Model.jpg There are four entities here:
JSMOLLContainer (abstract entity):
  Contains most of the logic of inserting, deleting, reordering, etc.
  Relationship:
     Nodes: To-Many relationship
            Delete Rule: cascade (removing the container removes the nodes)
ThingContainer (subentity of JSMOLLContainer):
  You can replace this with your subentity, including any attributes you might want to add.
JSMONode(abstract entity)::
  Contains only getters and setters for next and previous.
  Relationships:
     Container (JSMOLLContainer):
            Delete Rule: Nullify
     Next (JSMONode):
            Delete Rule: Nullify
     Prev (JSMONode):
            Delete Rule: Nullify
Thing (subentity of JSMONode):
  You can replace this with your subentity, including any attributes you might want to add.  I've chosen to add a "name" attribute (a string) as a demonstration.
Implementation

I chose to use Jonathan "Wolf" Rentzsch's mogenerator to create default implementation files. It's awesome. Default accessors are built for you and placed in a machine-generated superclass for each of your objects. Mogenerator's code keeps the managed object model consistent for us and allows us to implement specific functionality in our class files. This is magic.

First tries are always throw away code, right? When I tried to implement a double linked list through messaging the node objects (say calling setNext directly on a node object to insert a new node), things became difficult in a hurry. Basic functionality was simple; the problems started creeping in when I realized that I had to guard against a lot of strange edge cases when inserting and removing objects. Coredata objects like to stick around for undo / redo purposes, so I was constantly making sure I didn't call any objects that responded to isDeleted with YES. Needless to say, I couldn't get the unit tests to work reliably talking just to the nodes.

If one tries to implement a double linked list by forcing the user to message a managing container (something we're used to doing with arrays anyway), life gets much simpler. Here we quickly rub up against a very nice feature of coredata (and also the most frustrating thing with regard to debugging). It will happily fire inverse relationships for us to keep the object graph consistent. When we call setNext on our JSMONode subclass, coredata calls setPrevious on the inverse entity. If we forget that core data is doing that for us, we get into infinite loops.

Here's the interface file for the custom parts of the Container

 
#import "_JSMOLLContainer.h"
//_JSMOLLContainer is the machine generated code from mogenerator.  It has only
//the accessors and the KVO notifications within.

@interface JSMOLLContainer : _JSMOLLContainer 
{
    //we cache first and last values because the fetch takes a while
    @private
    JSMONode *_cachedFirst;
    JSMONode *_cachedLast;
}

- (void)addNodesObjectAtFront:(JSMONode*)value_;
- (void)addNodesObjectAtEnd:(JSMONode*)value_;
- (void) moveNode: (JSMONode *)moveMe
           before: (JSMONode *)beforeMe;
- (void) moveNode: (JSMONode *)moveMe
            after: (JSMONode *)afterMe;
- (JSMONode *)first;
- (JSMONode *)last;
- (NSArray *)nodesArray;
- (NSEnumerator *)nodesEnumerator;
- (NSEnumerator *)reverseNodesEnumerator;
@end
Adding a node


To add a node object we first add it to our collection of nodes by calling the mogenerater generated superclass method. This fires all of the appropriate KVO/KVC notifications. You'll no doubt notice that there are ivars (_cachedFirst and _cachedLast) for caching the first and last nodes in the chain. It turns out that we need to know first and last pretty often. Fetching this from core data can be painfully slow, so we cache this information when we can.
- (void)addNodesObject: (JSMONode*)value_
{
    //add node object at the END of the chain.
    [super addNodesObject: value_];
    JSMONode *last = [self last];
    JSMONode *newNext = [last next];  // should be null
    [value_ setPrev: last];
    [value_ setNext: newNext];
    [self set_cachedLast: value_];
    if (IsEmpty(last))
        [self set_cachedFirst:value_];
}
There is a similar method for putting a node object at the head of the chain.

Removing a node


To remove a node object we must first remove it from the superclass (this removes it from our collection) and set the bordering objects' next and previous values appropriately to keep the chain consistent. The appropriate inverse relationship methods are fired for us. (Remember those infinite loops I was talking about?).
- (void)removeNodesObject: (JSMONode*)value_ 
{
    [super removeNodesObject: value_];
    //set next and prev as needed on bordering objs
    if (!IsEmpty([value_ prev]))
        [[value_ prev] setNext: [value_ next]];
    else
        [[value_ next] setPrev: [value_ prev]];
}
Reordering
The easiest way to reorder a node is first to remove it (thus keeping the chain consistent with regard to adjacent objects), then tell the new previous node to set its next object to the node being moved and tell the node being moved to set its next object accordingly. (At 3:00 AM, this gets hard on the mind).
- (void) moveNode: (JSMONode *)moveMe
           before: (JSMONode *)beforeMe
{
    [self removeNodesObject: moveMe];
    
    JSMONode *myNewPrev = [beforeMe prev];
    if(!IsEmpty(myNewPrev))
    {
        //if we are not moving moveMe to the front of the chain, this works
        //otherwise there IS NO myNewPrev and we have to use the method below.
        [myNewPrev setNext: moveMe];
        [moveMe setNext: beforeMe];
    }
    else
    {
        [moveMe setPrev: myNewPrev];
        [beforeMe setPrev: moveMe];
    }
}
The rest of the implementation deals with building up simple enumerators and also with fetching the first and last nodes (where prev is Null or next is Null respectively) from the Managed Object Context when it turns out we haven't cached it appropriately. Eventually, the fetch requests will go away. Right now, they'll only likely be needed when we've done some reordering as that code doesn't update the cached first and last values as it should. To make sure that we're really returning the first node, we simply check that the _cachedFirst node's prev value is Null (actually [NSNull null]); that's what IsEmpty does here.
- (NSArray *)_nodesFilteredUsingPredicate: (NSPredicate *)predicate
{
    if(0 == [[self nodesSet] count]) return [NSArray array];
    NSEntityDescription * entity = [NSEntityDescription entityForName: @"JSMONode" 
                                               inManagedObjectContext: [self managedObjectContext]];
    NSFetchRequest * fetch = [[NSFetchRequest alloc] init]; 
    [fetch setEntity: entity]; 
    [fetch setPredicate: predicate]; 
    
    NSManagedObjectContext *context = [self managedObjectContext];
    NSArray * results = [context executeFetchRequest: fetch error: nil];
    [fetch release];
    return results;
}

- (JSMONode *)first
{
    if(0 == [[self nodesSet] count]) return nil;
    //If we haven't yet cachedFirst we need to find it.
    //IF cachedFirst previous value is not null, cachedFirst is no longer actually first.
    if (!IsEmpty(_cachedFirst) && IsEmpty([_cachedFirst prev]))
        return _cachedFirst;
    
    JSMONode *first;
    NSPredicate *predicate = [NSPredicate predicateWithFormat: @"container == %@ AND prev == %@", self, [NSNull null]];
    NSArray *results = [self _nodesFilteredUsingPredicate: predicate];
    
    if (0 == [results count])
    {
        [self set_cachedFirst: nil];
        return nil;
    }
    
    NSAssert(([results count] == 1), @"There should be one or fewer firstThing(s)");
    first = [results objectAtIndex: 0];
    [self set_cachedFirst: first];
    return first;
}

- (JSMONode *)last
{
    if(0 == [[self nodesSet] count]) return nil;
    if (!IsEmpty(_cachedLast) && IsEmpty([_cachedLast next]))
        return _cachedLast;
    
    JSMONode *last;
    NSPredicate *predicate = [NSPredicate predicateWithFormat: @"container == %@ AND next == %@", self, [NSNull null]];
    NSArray *results = [self _nodesFilteredUsingPredicate: predicate];    
    if (0 == [results count])
    {
        [self set_cachedLast: nil];
        return nil;
    }
    
    NSAssert(([results count] == 1), @"There should be one or fewer lastThing(s)");
    last = [results objectAtIndex: 0];
    [self set_cachedLast: last];
    return last;
}
Meanwhile, back at the Hall of Justice: usage So how do we use it? We simply insert a JSMOLLContainer subclass (in the case of the diagram above, it's a ThingContainer) into the managed object context like so:

container = [NSEntityDescription insertNewObjectForEntityForName:@"ThingContainer" 
                                                               inManagedObjectContext:moc];


Then we start putting node objects into it like so:

thingZero = [[NSEntityDescription insertNewObjectForEntityForName:@"Thing" 
                                                inManagedObjectContext:moc]

     //perhaps we want to set an attribute
     [thingZero setName:@"Thing Zero Here"];
     [container addNodesObject:thingZero];

     thingOne = [[NSEntityDescription insertNewObjectForEntityForName:@"Thing" 
                                                inManagedObjectContext:moc]

     //perhaps we want to set an attribute
     [thingOne setName:@"Thing One Here"];
     [container addNodesObject: thingOne];
Now we've a container with two things in this order: thingZero -> thingOne.

If we wanted to iterate through a list, we have a couple of options: 1. We can ask the container for its object enumerator and do it "cocoa style"
    JSMONode *currentThing;
    NSEnumerator *nodesEnumerator = [container nodesEnumerator];
    while (currentThing = [nodesEnumerator nextObject])
    {
        //do something
    }

2. We can ask the container for the first object and keep asking for next
    JSMONode *currentThing = [container first];
    
    while (!IsEmpty(currentThing))
    {
        //do something
        currentThing = [currentThing next];
    }

At this point, I should probably show you the IsEmpty function. This is mostly stolen from Wil Shipley's blog . I just added the test for [NSNull null], which is the "empty" object for insertion into cocoa collections (like a core data graph, for instance).
//Thanks Wil
static inline BOOL IsEmpty(id thing) {
    return thing == nil
    || ([thing isEqual:[NSNull null]]) //JS addition for coredata
    || ([thing respondsToSelector:@selector(length)]
        && [(NSData *)thing length] == 0)
    || ([thing respondsToSelector:@selector(count)]
        && [(NSArray *)thing count] == 0);
}


Inserting an object before or after another object currently in the chain is (for now) a two step process. Let's say you have a node called "node3" in a linked list and you want to add a node called "node7" to the list after node 3. Let's pretend that we have nodes zero through six already in the list. (node0 -> node1 -> node2 -> ... node6) node7 (outside)

1. Put node7 in the container by simply adding it to the end
[container addNodesObject: node7]
2. Then place it after node3
[container moveNode: node7
                   after: node3;]


That's it. It's somewhat suboptimal in that the container will insert at end, then remove, then reinsert the node behind the scenes, but I haven't had to optimize this code yet in practice and it keeps the API simple.

So there you have it. Make a subentity (or several) of the JSMOLLContainer and the of JSMONode objects and enjoy. Take a look at the subversion repository. It includes test code that will give you a good idea of how everything works in practice. Oh. Do what you want with it. It's offered AS IS. No Warranty.

November 11, 2006

Graphviz rocks

I needed to come up with an innovative (pun intended, for those who get the joke) way of tracking time by task for one of my clients recently. I considered dusting off DIA or perhaps omnigraffle, but I realized that the tedium involved in laying out the hierarchical structure I wanted to see would probably kill me. So, I thought I would give Graphviz a shot.

The nice thing here is that it will lay out a hierarchy for you and flow your chart in an efficient manner. I would have gone slowly mad doing this by hand.

Click on the image below to see a larger version.


GraphvizLG.jpg

You can see the markup I gave graphviz in the extended portion of the post.

Continue reading "Graphviz rocks" »