Main

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...)

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...)

December 04, 2008

Enumerating and visualizing all fonts on iPhone

When you're designing custom views or otherwise theming your iPhone application, it's nice to be able to see all of the available fonts. I couldn't find anything online that showed them, so I made my own little application. You can get it at FontListViz.tgz.



Screenshot 2008.12.04 01.27.59.jpg



It's pretty well dirt simple. Get all font family names, then get all of the font names in each family and store them in an array, then put each font name's text in a table view cell and set the table view cell's font using -[UIFont fontWithName:size:] and, well, Bob's your uncle -- instant very simple font viewer.

- (void)viewDidLoad 
{
    [super viewDidLoad];
    self.fontNames = [NSMutableArray array];
    NSArray *fontFamilyNames = [UIFont familyNames];
    for (NSString *familyName in fontFamilyNames) {
        NSLog(@"familyName = %@", familyName);
        NSArray *names = [UIFont fontNamesForFamilyName:familyName];
        NSLog(@"FontNames = %@", fontNames);
        [self.fontNames addObjectsFromArray:names];
    }
}
.
.
.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
{
    
    static NSString *CellIdentifier = @"Cell";
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease];
    }
    
    NSString *name = [self.fontNames objectAtIndex:indexPath.row];
    cell.text = name;
    cell.font = [UIFont fontWithName:name size:14];
    return cell;
}

Enumerating and visualizing all fonts on iPhone

When you're designing custom views or otherwise theming your iPhone application, it's nice to be able to see all of the available fonts. I couldn't find anything online that showed them, so I made my own little application. You can get it at FontListViz.tgz.



Screenshot 2008.12.04 01.27.59.jpg



It's pretty well dirt simple. Get all font family names, then get all of the font names in each family and store them in an array, then put each font name's text in a table view cell and set the table view cell's font using -[UIFont fontWithName:size:] and, well, Bob's your uncle -- instant very simple font viewer.

- (void)viewDidLoad 
{
    [super viewDidLoad];
    self.fontNames = [NSMutableArray array];
    NSArray *fontFamilyNames = [UIFont familyNames];
    for (NSString *familyName in fontFamilyNames) {
        NSLog(@"familyName = %@", familyName);
        NSArray *names = [UIFont fontNamesForFamilyName:familyName];
        NSLog(@"FontNames = %@", fontNames);
        [self.fontNames addObjectsFromArray:names];
    }
}
.
.
.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
{
    
    static NSString *CellIdentifier = @"Cell";
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease];
    }
    
    NSString *name = [self.fontNames objectAtIndex:indexPath.row];
    cell.text = name;
    cell.font = [UIFont fontWithName:name size:14];
    return cell;
}

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)).

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" »

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" »

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" »

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

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

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.

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

Resolution Independence

It's not just for icons. :)

There is some talk of resolution-independence these days, and for good reason. Them pixels is getting smaller.

Those of us who like to cheat and use 2D impostors for 3D objects get to be a little more careful these days as well. Luckily, thanks largely to information I got at the Big Nerd Ranch OpenGL class, one can easily make the 3D equivalent of vector graphics in something like (say) maya or wings3D and wrap them up in a display list in openGL for speed of rendering.

This makes one's visualization happily zoomable and resolution-independent without much of a performance hit. Just load your model in your initialization and wrap the drawing in a display list.

My pet project at a normal zoom level:

graphModels.jpg

and zoomed waaaaay in:

zoooom.jpg

Can you tell which part of the graph is still a stretched image + simple texture?

Resolution Independence

It's not just for icons. :)

There is some talk of resolution-independence these days, and for good reason. Them pixels is getting smaller.

Those of us who like to cheat and use 2D impostors for 3D objects get to be a little more careful these days as well. Luckily, thanks largely to information I got at the Big Nerd Ranch OpenGL class, one can easily make the 3D equivalent of vector graphics in something like (say) maya or wings3D and wrap them up in a display list in openGL for speed of rendering.

This makes one's visualization happily zoomable and resolution-independent without much of a performance hit. Just load your model in your initialization and wrap the drawing in a display list.

My pet project at a normal zoom level:

graphModels.jpg

and zoomed waaaaay in:

zoooom.jpg

Can you tell which part of the graph is still a stretched image + simple texture?

October 30, 2006

implicit HIG: toys from others

As apparently have a number of other developers/bloggers, I've been reading and thinking about the implicit HIG since John Gruber spoke on the topic at the C4 conference. It seems the consensus thus far is:

1. Apple isn't likely to help.

Unfortunately, this raises the barrier to entry if you don't want Gruber to call you up with the words "2001 called. They want their UI back."

2. It would be nice if there were one place to go for example code for these kinds of things.

I'm relatively new on the Mac Geek scene and, given the personalities involved and some of the other fantastic resources already out there (links here to the various mailing list aggregations), am surprised that there isn't a community code repository for Cocoa programmers to post / retrieve solutions to common development problems like the implicit HIG.



So! Let's do something about this. With the above in mind, I've started keeping a list of links on del.icio.us where I've found interesting and/or slick cocoa widgets and frameworks using the tag newHIG. I'm hoping that other developers out there who have solved a common Cocoa development problem like this will be willing to share links and code. Once the number of newHIG tags I have hits a critical mass, it's time to set up a wiki / repository for this stuff. I'll do it. If anybody has any ideas or would like to donate some code, some time, or some beer, shout!

So far, I've tried only one of the solutions I've found and it seems "Batteries included" to me. The new ilifecontrols framework from Sean Patrick O’Brien simply rocks. All I had to do was embed the framework in my little test app, drag and drop the headers into IB, and set the appropriate classes for widgets and windows. Five minutes of work and my app is skinned iLife style. Odd how the visual change already makes it seem more fun to play around with this little app. I must be a fanboy now, huh?

You've seen the visualization I'm working on just a few times now.

Before:

oldUI.jpg

After:

newUI.jpg

No, it's not a pretty app to begin with; it's a test app. (By the way, the fps numbers are meaningless after the view has drawn. The framework did not slow anything down.) Hopefully though, this gives us an idea of what one can do in a hurry with the framework. Rock on Sean.

In the next few days, I'll start dropping others' widgets into my work to see how they look together. There are a bunch of nice widgets and little "here's how you do this or that" from Matt Gemmell, including an i-tunes 5/6 window style, that look awesome as well. There is a nice little "Gear menu" from RogueSheep's Bleatings (baaaaaah). Considering the above, it looks like our implicit HIG toolkit is well underway.


#!/usr/bin/env python
#for the developers who write code so I don't have to
for i in xrange(1000000):
print "thanks"

October 23, 2006

C4 was explosive

No, NSA, not that C4. I'm talking about the conference I went to this weekend full of mac programmers. Sheesh.

First, wowie zowie many thinks to Rentzsch. Not only is this guy wicked-smaht, he can put on a conference with 100 attendees without breaking a sweat. Well, at least it looked easy.

I met a bunch of awesome people, many of them bloggers I read religiously. Apparently, John Gruber shares my inability to leave IKEA without melting a credit card, which somehow makes me feel better about myself. Daniel Jalkut who was awarded a music degree (wee!) last year, and I had a great time talking about how surviving a jury in music school makes it impossible to feel all that nervous for any job interview or presentation ever again. Duncan Davidson shoots some fine pictures. Thanks for shooting little ol' me while you were at it, Duncan!

276484783_8afa31fe80.jpg

I met a bunch of other developers and everybody left me thinking "what a helluva nice guy/gal."

Some of the topics have me thinking, so I'm sure I'll be spilling a few pixels on them. I'm newly interested in ways of easing concurrency in app development, thanks of course to Steve Dekorte for the brain food. My new old friend from the Big Nerd Ranch Aaron Hillegass has ambushed my brain into thinking with big thoughts. ...again.

Oh! And this is my first post in MarsEdit, which is hacked by two presenters from the conference Brent Simmons of NetNewsWire fame and (for a time) Gus Mueller who founded Flying Meat Software. This is a great app. Where's my credit card?

C4 was explosive

No, NSA, not that C4. I'm talking about the conference I went to this weekend full of mac programmers. Sheesh.

First, wowie zowie many thinks to Rentzsch. Not only is this guy wicked-smaht, he can put on a conference with 100 attendees without breaking a sweat. Well, at least it looked easy.

I met a bunch of awesome people, many of them bloggers I read religiously. Apparently, John Gruber shares my inability to leave IKEA without melting a credit card, which somehow makes me feel better about myself. Daniel Jalkut who was awarded a music degree (wee!) last year, and I had a great time talking about how surviving a jury in music school makes it impossible to feel all that nervous for any job interview or presentation ever again. Duncan Davidson shoots some fine pictures. Thanks for shooting little ol' me while you were at it, Duncan!

276484783_8afa31fe80.jpg

I met a bunch of other developers and everybody left me thinking "what a helluva nice guy/gal."

Some of the topics have me thinking, so I'm sure I'll be spilling a few pixels on them. I'm newly interested in ways of easing concurrency in app development, thanks of course to Steve Dekorte for the brain food. My new old friend from the Big Nerd Ranch Aaron Hillegass has ambushed my brain into thinking with big thoughts. ...again.

Oh! And this is my first post in MarsEdit, which is hacked by two presenters from the conference Brent Simmons of NetNewsWire fame and (for a time) Gus Mueller who founded Flying Meat Software. This is a great app. Where's my credit card?

June 07, 2006

Bob

I don't know how he finds the time, but the python / mac community owes Bob several rounds of applause. Let me be the first to stand (soon as I get this here foot outta mah mouth). I'm also wowed at how he managed to find my blog and comment before I got round to sending a question to the mailing lists. And ... well you know... he kicked my butt and I deserved it.

June 06, 2006

PyObjC proxy fun

PyObjC is cool. Really. No Kidding. What I'm about to say, though, is making me think that my friend Patrick www.patrickkidd.com has a better idea in using pyQT for gui work...

When I'm using an external library, say perhaps one that sends information over a network, I try to be careful to keep from passing the lower-level code anything that could muck things up. Well... it turns out that it's nearly impossible to keep python's object types from being converted to pyobjc objects. This isn't too surprising, given that many ints, strings, etc. have to make their way into objc arguments, though I was making the improper assumption that python objects were converted through some kind of transparent proxy only when crossing that boundary to the objc runtime. I find out now that python objects are sometimes just converted. That being the case, I didn't expect those converted objects to act like anything but your typical python object from the point of view of python (ala duck typing... sorta). Again. WRONG. Somehow, for instance, the integer type sometimes turns into an OC_PythonInt (which has a method intValue() to get to the python int it represents, FYI) and it doesn't have the same methods as a python int. Bummer. It's kinda hard to send that over the network.

What to do?

"assert" statements, I guess.

May 18, 2006

I did, I did ate a Zombie

From Ronald Oussoren to me (+ pyobjcdev and pythonmac-sig):

I haven't look at your example just yet, will do that later on. I'm
happy you took the time to create a small program that demonstrates
the problem you're seeing, that makes is so much easier to search the
problem.

> What did I learn from this:
>
> 1. Try to use cocoa objects instead of python objects in code that
> uses bindings; pay special attention to collections (lists, etc.).
> At least this *seems* safer because a python object being
> referenced from cocoa is probably more likely to cause problems
> than a cocoa object controlled from python. (?) Then again, it's
> pretty stupid to generalize from one experience...

Using Cocoa datastructures instead of python ones when KVO is
involved is always a good idea. Python doesn't have enough hooks to
allow reliable interception __setitem__. We could (but don't)
get 95% of the way by using the same class-swiffling technique as
used by the ObjC runtime, but could never go all the way.

> 2. GDB is very useful for debugging pyobjc.
> 3. There are a lot of nice pyobjc debugging features, but you have
> to turn them on (See my example project referenced above).
>
> Things I want to know:
> 1. Is there a way, when one gets the memory address of a pyobjc
> proxy object (Like OC_PythonArray) from gdb to figure out which
> python object it's proxying?
The PyObject* is stored as the instance variable 'value' on the proxy
object.

> 2. Is there a way to get the python object from a memory address?

You mean like "(PyObject*)0xDEADBEEF"?

> 3. Is there a reliable way to get (in gdb or pdb) the "Python List
> Object at 0x03423blahblahblah information from the python object
> being proxied

> 4. Can I compile pyobjc with debugging symbols? GDB without them
> hurts my brain.

PyObjC is compiled with debugging symbols by default. Py2app strips
debugging symbols, unless you build with the -A flag.

Ronald

May 04, 2006

I think I just ate a Zombie

Well, I think I've figured out the *WTF* part of my little NSZombie problem.

It really boils down to this:

Bind an NSPopubButton (contents) to a Python List object that is an attribute of a python class placed in an NSArray controlled by an NSArrayController and the OC_PythonArray object from PyObjC corresponding to the Python List gets freed too early. (There's a mouthful... Confusing enough?) Wrap that Python List object in an NSArray.alloc().initWithArray_copyItems_([u'the', u'list'], objc.NO) and (I guess) the NSArray...init... adds one more -retain than does OC_PythonArray and the binding doesn't break.

I'm always nervous to call things like this a bug because maybe I'm not *supposed* to be able to do what I'm trying to do in the way that I'm trying to do it, but this one looks like a bug to me. (With the caveat that I am currently learning pyobjc/cocoa and don't always know the *right* way to do things...)

Continue reading "I think I just ate a Zombie" »

April 10, 2006

NSZombie eating my brain!

I've been learning cocoa bindings (I like this voodoo) and have hit a brick wall. I've been asking pythonmac-sig if they've any ideas and they pointed me in the direction of NSZombieEnabled, but i haven't been able to get very far.

I have an NSArray controller with a python object subclassing NSObject. I've put a + button on the gui to add the python object like this:::

def addTriggerModel_(self, sender): # BUTTON
self.setCanAddTriggerModel_(False)
newTriggerModel = scNSDrumTriggerModel.alloc().init()
newTriggerModel.resetWithNodeBusAndInput(node = self.nextNode, bus = self.nextBus, \
input = self.nextInput, sendInfo = self.sendingTriggersToClient)
self.nextNode += 100
self.nextBus += 10
self.nextInput += 1
NSLog("Adding newTriggerModel")
self.triggerArrayController.addObject_(newTriggerModel)
NSLog("Added newTriggerModel")
self.setCanAddTriggerModel_(True)


This works just peachy the first time I do it, but fails on a second addition of the new scNSDrumTriggerModel object. At first I was getting Signal 5/6 errors, then I did:

from PyObjCTools import Signals
Signals.dumpStackOnFatalSignal()
from PyObjCTools import Debugging
Debugging.installVerboseExceptionHandler()

This told me:

NSGenericException - *** Selector 'retain' sent to dealloced instance 0x1147cb0 of class OC_PythonArray

So I turned on NSZombieEnabled and tried running in gdb with a breakpoint on -[_NSZombie retain], but I don't really know what I'm looking for. I'm looking at a lot of hex memory addresses with very little human-readable stuff in there.

I tried setting a breakpoint at (+[OC_PythonArray newWithPythonObject:]) to see perhaps what proxy objects are being instantiated, but I'm clearly in over my depth. The variable corresponding to the pythonObject in OC_PythonArray newWithPythonObject: up there ^ has address 0x0 (Is that a null pointer?) in gdb each time I hit that breakpoint.

When I run malloc_history on the memory address that the NSZombie has, I get this:

Continue reading "NSZombie eating my brain!" »

February 26, 2006

xCode and pyObjc "build and run"

For those of us who live with pyobjc's xcode templates, there is an annoying little bug when creating a new project, at least on my machines. There are no executables created when you make a new python/cocoa project of any kind which causes xCode to disable "build and run" button. Sure, we can build in Xcode, navigate to the build directory, and run the application, but that's what the "build and run" button is for! How do we fix this? Add the project executable to the Executables group in xcode!

Like this:

In your xCode session, there is a pane down the left side that shows the contents of the project. In that pane, ctrl-click the "Executables" group, select "Add," then "New Custom Executable". Put the name of your application appended by .app in the "Executable Name field." (i.e. if your app is called "Trigger," put "Trigger.app" in this field). Use the "choose" button to navigate to your application's build directory on your hard disk and choose your application bundle. (If it doesn't exist, try building your application with the build button once first.) Now in the "Project" menu of xCode, "Set Active Executable" to your application name. This should enable the "build and run" button.

UPDATE: Once you've done that ^, you can then change the path to the executable to $BUILT_PRODUCTS_DIR/YourApplicationNameHere.app so that your project doesn't have any hard-coded paths.

Cocoa Bindings 2: not too frustrating, really

If you thought it was cool to drag connections between GUI widgets and actions in a controller object when you first started using Cocoa, you’ll flip when you see this in action. Bindings really are just what they say they are: tell Interface Builder to connect this GUI widget to this part of the data model and, presto-chango, no significant glue code.

I have a little PyObjC application that is coming along quickly thanks to cocoa bindings. My first PyObjC app took me weeks to figure out (partly due to the lack of cocoa object knowledge) and is several hundred lines of code. This app was started a few days ago, is considerably more complex, is nearly finished, and is barely any code.

Info regarding dead-ends I hit and how I’ve gotten this to work for me will be posted here soon. I may post a pseudo-wiki sort of thing that clarifies some of the things I found confusing while trying to use this in a real application. For now, here are links to the tutorials I’ve found most helpful:


This one is dirt simple, but clarifies a lot of the voodoo involved:
http://bob.pythonmac.org/archives/2004/04/04/pyobjc-nstableview-cocoa-bindings-tutorial/


This one is down right now, but was up yesterday. I assume they’re having temporary problems. This is a bummer as this tutorial shows one how to get and set values directly in the data model without a controller object.

http://www.wilcoxd.com/blog/archives/000075.html

February 23, 2006

Cocoa Bindings 1: the resolution

OK. It's a little late for a resolution for the New Year.

I am finally going to learn cocoa bindings. I have the right app for it. I've a couple of books with info. I've got tutorials all over my dev documentation folder.

Come along. I'll chronicle the ride here.

It's going to happen. I can feel it. This time, PyObjC. Next time, objc

jonathansaggau.com/trigger.jpg