thingsinjars

  • 1 Nov 2009

    Building an Objective-C growlView

    Wherein our intrepid hero learns some Objective-C and figures out the easy bits are actually quite hard.

    This is a little guide on how to write a growl plugin to automatically send notifications as tweets. If you just want the finished thing: GrowlBird.zip [Zip - 228KB]

    A couple of days ago, I decided to give myself a little software development task to write a Twitter Growl view. Growl is a centralised notification system for Mac OS X that lots of other applications can use so that there's one consistent way of showing notifications.

    Background

    The idea behind this little experiment wasn't to have tweets appear as growl notifications, there are already plenty of apps that do this, the idea was to have growl notifications sent to Twitter. Some friends have started organising a crowdsourced Friday afternoon playlist via Spotify and I thought it'd be handy if Spotify tweeted each song as it started. The easiest way I could think of doing this was to tap into the fact that Spotify sends a Growl notification on track start and get the Growl display plugin to tweet it as well [1].

    Build

    I downloaded the Growl Display Plugin Sample 1.2 [Zip - 186 KB] from the developer downloads page and the MGTwitterEngine library. I then downloaded Xcode so I could do the development. I have to point out here that this was my first foray into Objective-C programming and, indeed, my first attempt at anything vaguely C-related since I wrote a command-line calculator about 12 years ago. If I do it wrong, please forgive me.

    The first thing to do was open the sample project in Xcode, figure out what files do what, etc. There is very little documentation on how Growl views or display styles work so I pretty much just spend an hour reading all the source from top to bottom. Here's a quick summary:

    Sample_Prefix.pch
    Pre-compiled header. Stuff that's included before every pre-compiled file
    Growl/
    Folder containing standard Growl stuff. Don't need to touch.
    GrowlSampleDisplay.h
    Header file, didn't need to change anything
    GrowlSampleDisplay.m
    Class for setting up things. Again, didn't touch [2].
    GrowlSamplePrefs.h
    Defining default preference values and naming functions to handle them. More on this later.
    GrowlSamplePrefs.m
    The actual functions mentioned in the previous header file
    GrowlSampleWindowController.h
    Not doing anything visual, really so I didn't need to mess around with this
    GrowlSampleWindowController.m
    As above
    GrowlSampleWindowView.h
    Declaring objects needed for execution
    GrowlSampleWindowView.m
    Instantiating the objects then actually using them later on.

    Again, I'm not used to doing this stuff so if I'm using the wrong terminology, just pretend I'm not.

    I then dragged the MGTwitterEngine library into the project drawer, saved and built. At this point it successfully did nothing different which is what I was hoping it would do. Well, it popped up the 'This is a Preview of the Sample Display' message using the MusicVideo style which is what it does when you don't screw with it.

    Testing growlView

    It's not actually that easy to test growlView plugins. You can't simply add and remove them. To install, you double-click on it. You should be able to see it listed in the Display tab of the preference pane. To re-install, you need to delete ~/Library/Preferences/com.Growl.GrowlHelperApp, ~/Library/Application Support/Growl/Plugins/Sample.growlView and restart growl (click on the paw in the menu bar). It's a bit of a hassle.

    The next thing was to include the MGTwitterEngine. In GrowlSampleWindowController.h, #import "MGTwitterEngine.h" and create a new object. I just followed the instructions in the README but be sure to follow all of them. If you get errors about LibXML not being installed or YAJL not working, don't worry, you just need to make sure you set USE_LIBXML to 0 in all the places you're supposed to. GrowlSampleWindowController.h now contains this:

    
    #import "GrowlDisplayWindowController.h"
    #import "MGTwitterEngine.h"
    
    @class GrowlBirdWindowView;
    
    @interface GrowlBirdWindowController : GrowlDisplayWindowController {
    	CGFloat						frameHeight;
    	NSInteger					priority;
    	NSPoint						frameOrigin;
    	MGTwitterEngine *twitterEngine;
    }
    @end

    In GrowlSampleWindowController.m, I then instantiated the new object:

    
      @implementation GrowlBirdWindowController
      - (id) init {
      	  :
      	  :
          twitterEngine = [[MGTwitterEngine alloc] initWithDelegate:self];
    	    [twitterEngine setUsername:@"growlbirdtest" password:@"testgrowlbird"];
    	  }
    	  :

    And then modified the setNotification function to also send an update:

    
    - (void) setNotification: (GrowlApplicationNotification *) theNotification {
        :
    	[view setTitle:title];
    	[view setText:text];
      NSLog(@"sendUpdate: connectionIdentifier = %@", [twitterEngine sendUpdate:[NSString stringWithFormat:@"%@, %@", title, text]]); // The new line
      :
      }

    That was enough to get growl to send messages to appear on http://twitter.com/growlbirdtest but it doesn't make it that useful for anybody else, to be honest. The next thing to figure out was the preferences.

    Preferences

    Without documentation, this took a bit longer that I expected. To start off changing the english version before worrying about localization, find the GrowlBirdPrefs.xib in resources/en.lproj/ and open it. Interface Builder will launch then you can double-click on 'Window' and see the layout of the preference pane. Search in the Library for 'text' and drag a text field into the window then spend about half and hour clicking round the interface. Open up the various inspectors (right-click on an object), look through the different tabs, click between the newly added text field and the sliders and drop-downs that are already there just to see what's different. Once I was a bit familiar, I opened the connections tab so that I could bind the value of the text field to the value 'twitterUsername' in my code. I checked 'value', Bind to 'File's Owner' and entered 'twitterUsername' in Model Key Path. I then repeated this for twitterPassword using a Secure Text Field from the Library. The option nextKeyView is used to say which item is tabbed to next when you're navigating with the keyboard so to keep things tidy, I dragged lines from nextKeyView from each of them to the right places in the layout.

    Back in the code, I added new default preferences in GrowlSamplePrefs.h:

    
      #define Sample_USERNAME_PREF		@"Username"
      #define Sample_DEFAULT_USERNAME		@"growlbirdtest"
    
      #define Sample_PASSWORD_PREF		@"Password"
      #define Sample_DEFAULT_PASSWORD		@"testgrowlbird"
      :
      :
      @interface GrowlBirdPrefs : NSPreferencePane {
      	IBOutlet NSSlider *slider_opacity;
      	IBOutlet NSString *twitterUsername;
      	IBOutlet NSString *twitterPassword;
      }

    and named some handlers for them:

    
      - (NSString *) twitterUsername;
      - (void) setTwitterUsername:(NSString *)value;
      - (NSString *) twitterPassword;
      - (void) setTwitterPassword:(NSString *)value;

    Be careful here, I got confused and didn't have the same spelling here for twitterUsername and twitterPassword as I had put in the interface builder as I hadn't realised the two were directly connected. They are. Obviously. The next thing to do is to write the code for these handlers:

    
      - (NSString *) twitterUsername {
      	NSString *value = nil;
      	READ_GROWL_PREF_VALUE(Sample_USERNAME_PREF, SamplePrefDomain, NSString *, &value);
      	return value;
      }
      - (void) setTwitterUsername:(NSString *)value {
      	WRITE_GROWL_PREF_VALUE(Sample_USERNAME_PREF, value, SamplePrefDomain);
      	UPDATE_GROWL_PREFS();
      }
      - (NSString *) twitterPassword {
      	NSString *value = nil;
      	READ_GROWL_PREF_VALUE(Sample_PASSWORD_PREF, SamplePrefDomain, NSString *, &value);
      	return value;
      }
      - (void) setTwitterPassword:(NSString *)value {
      	WRITE_GROWL_PREF_VALUE(Sample_PASSWORD_PREF, value, SamplePrefDomain);
      	UPDATE_GROWL_PREFS();
      }
    

    Build and reinstall and this will now show the same preference pane as before but with two new text fields which allow you to enter your username and password. In fact, build at several stages along the way. Every time you make a change, in fact. If something breaks, check the error log to see if it's something predictable that should have broken at that point or if you've done something wrong. Also, keep the OS X log app Console open in the background. It will spew out error messages if you do something wrong. It's also good to have your code write out console messages to keep a track on what your code is doing like so:

    
      - (NSString *) twitterPassword {
      	NSString *value = nil;
      	READ_GROWL_PREF_VALUE(Bird_PASSWORD_PREF, SamplePrefDomain, NSString *, &value);
      	NSLog(@"twitterPassword = %@", value);
      	return value;
      }
    

    You'll notice we're still sending messages to the growlbirdtest account because, even though we are reading and saving the username and password, we're not doing anything with them. That's easily remedied by editing GrowlSampleWindowView.m again and replacing the hard-coded login details with a couple of lines to read from the preferences or fall back on the default:

    
      twitterEngine = [[MGTwitterEngine alloc] initWithDelegate:self];
    	NSString *twitter_username = Bird_DEFAULT_USERNAME;
    	NSString *twitter_password = Bird_DEFAULT_PASSWORD;
    	READ_GROWL_PREF_VALUE(Bird_USERNAME_PREF, SamplePrefDomain, NSString *, &twitter_username);
    	READ_GROWL_PREF_VALUE(Bird_PASSWORD_PREF, SamplePrefDomain, NSString *, &twitter_password);
    	[twitterEngine setUsername:twitter_username password:twitter_password];
    	NSLog(@"Twitter Login: username = %@", twitter_username);
    

    And, hooray! It works and posts to the account for which you entered details. Sort of. Some apps double-post. I haven't figured out why yet.

    Renaming

    After all that, the final bit (which I thought would be the easiest) was to rename the growlView from 'Sample' to 'Bird'. I have read that in the latest version of Xcode (which presumably comes with Snow Leopard), there's a global 'Rename' which will do all the relevant stuff for you. If you don't have that, you'll need to read 'On the Renaming of Xcode Projects' and do everything there. If you're still finding your growlView is called Sample, manually open every Info.plist you can find, 'Get Info' on everything, scour through the settings for the different build environments (Debug and Release)... It took longer to rename the project than to actually build it.

    A trivial aside, I have also added two fields to the preferences for tweet prefix and tweet postfix in exactly the same way the username and password were added. I leave the details to the interested reader.

    You should now have a completed, installable growlView/Growl View/Growl Display/growlStyle/whatever it's actually called. You can export the current Git project to have a look around or you can just download the finished GrowlBird.zip [Zip - 228KB] if you like. Note, the Git project isn't guaranteed buildable at any moment in time, I might break it. The localisations still need done and the layout of the prefPane isn't the greatest, either.

    [1] Since writing this, I have discovered that someone else already made a growlView to do this, it just didn't show up on any of my Google searching.

    [2] It turns out that I did have to modify these in the end due to a silly bug. Like I said, this is my first attempt at this kind of thing.

    Geek, Development

  • 25 Oct 2009

    It's not difficult, don't make it difficult

    What's easier? Boiling a single potato, letting it cool, mashing it using a toothpick then repeating with a different potato until you have a plateful of mashed potato...

    or

    Boiling all the potatoes you need at once then mashing them together with a potato masher?

    Okay, choose between one of these methods of determining whether the bathroom light is on: Draw up a list of people who have visited your house recently. Interview them to build a data set of all rooms visited and by whom. Re-interview those who visited the bathroom. Determine a timeline of bathroom visits and light switch position on entry and on exit. Analyse the data to find the last visitor to the bathroom and the position of the light switch. Examine the electrical connections between the light switch and the light bulb to determine what the current status of the light itself might be.

    or

    Go look.

    How are you doing on the quiz so far? Okay. So, final question: What's easier? Building a convoluted web site using proprietary code, conflicting CSS requiring you to target everything with !important, making all interaction rely on JavaScript for even the most basic functionality, fighting between form and function so much that you end up having a website that only works occasionally and even then only works for a subset of the available users.

    or

    Building a straightforward website using nothing but standard mark-up, styles which cascade in a predictable fashion and enhancing already-working functionality with a dash of JavaScript to make people go 'Ooh, shiny'?

    If you thought the second option was easier, I'm sorry, you would appear to be wrong. At least, that's the impression I get every single day while wandering round the internet. It must be really easy to make a ham-fisted, in-bred, should be kept in the basement monstrous-crime-against-nature abomination of a website because otherwise, people wouldn't do it so much.

    I've used Opera as my main browser for almost 10 years now and I've lost count of the number of times I've been faced with a message apologising to me because it appears my browser is out of date. If I could just update my browser to Internet Explorer 5, I could enjoy their site. Seriously, it must be a lot easier to make a web page locking me out of the site than not to. It must be a matter of a few seconds work to write browser-sniffing scripts and learn all the proprietary foibles of IE whereas not writing that script must take hours and not learning bad habits must take years.

    I have some ability to forgive those websites which are obviously the work of someone whose passion is something else. If I'm looking at a site where a guy has meticulously documented the different ways different cats react to different balls of yarn, I'm guessing his interests is in yarn. Or cats. Or the combination thereof. He's not necessarily going to know the best way to make a website. I find it much harder, however, forgive big companies. Either those with an in-house web staff or those who contract agencies. Whatever way they do it, someone is getting paid to make the website. It is someone's job to write code.

    I've always been of the opinion that if you're going to do any thing, you should at least try to do it as well as it can possibly be done. It doesn't matter if you're playing piano, rowing, juggling chickens or making a website, you have no excuse for not at least trying to be awesome at it. If you end up being awesome at it: great! You're the world's best chicken juggler, go into the world knowing that and be happy. If you don't: great! You gave it a darn good try and you probably ended up pretty good, at least. Maybe try juggling cats next time. I have a hard enough time getting my head around the idea that not everybody follows this same level of obsession in their interests but to have people who are actually getting paid actual money to do something (in this case, making a website, not chicken juggling) and who feel it's okay to be 'okay' is a concept I have great difficulty understanding.

    Okay, impassioned rant over. I'm not going to name any sites. Just consider this a warning, Internet.

    Opinion, Geek

  • 24 Sep 2009

    Ideas

    To continue from the post of a month ago about how Noodle was awesome and ahead of its time, I now have to point out Sidewiki. Darn it, Google. Couldn't you just have bought me out? I'd have sold. Quite cheap, too...

    Anyway. Onto the next idea. Or ideas.

    Keen-eyed regulars (those whose don't subscribe via RSS, anyway) will have noticed the new category for 'ideas'. I may as well put all these dumb little ideas I have out there and see if anyone wants to have a go at playing with any of them. Actually, those who subscribe via RSS may have been inundated earlier with a bunch of ideas as I uploaded the backdated ones. Sorry 'bout that.

    Geek

  • 6 Sep 2009

    Okay, unnecesary redesign

    Not two weeks after being pleased with myself that I could subtly rejig the design without only a few lines of CSS, I decided on Friday to completely redo this site.

    Not only did I change the layout but I've made some major changes under the hood, too. I decided to have my first attempt at an HTML5 page. Granted, it might just fall apart at any moment in any given browser but...hey, it might not.

    On the subject of HTML5, Mark Pilgrim (he of the 'Dive into...' series) brought up an interesting point in the WHATWG Blog last week on the topic of whether XHTML was actually a good idea in terms of enforcing XML syntax on an HTML document:

    It provides no perceivable benefit to users. Draconianly handled content does not do more, does not download faster, and does not render faster than permissively handled content. Indeed, it is almost guaranteed to download slower, because it requires more bytes to express the same meaning -- in the form of end tags, self-closing tags, quoted attributes, and other markup which provides no end-user benefit but serves only to satisfy the artificial constraints of an intentionally restricted syntax.

    And, I guess, it is a good point that a well-formed XHTML document will be larger than the equivalently well-formed HTML document. If, however, developers are given a strict set of rules and a strict validator and told "make your page according to these rules, this alarm will go off if you've done it wrong", they're less likely to fall into bad habits than if they are told "These are mostly rules but sometimes suggestions, this alarm will only go off if you got things very very wrong". Mark Pilgrim is, quite rightly, focusing on the user's point of view but it just seems to me that users will also benefit from more maintainable, better structured code.

    Of course, none of this actually matters yet and won't for the next five years or so. It probably won't matter then, either. It is only the interwebs, after all.

    Geek

  • newer posts
  • older posts

Categories

Toys, Guides, Opinion, Geek, Non-geek, Development, Design, CSS, JS, Open-source Ideas, Cartoons, Photos

Shop

Colourful clothes for colourful kids

I'm currently reading

Projects

  • Awsm Street – Kid's clothing
  • Stickture
  • Explanating
  • Open Source Snacks
  • My life in sans-serif
  • My life in monospace
Simon Madine (thingsinjars)

@thingsinjars.com

Hi, I’m Simon Madine and I make music, write books and code.

I’m the Engineering Lead for komment.

© 2025 Simon Madine