An Epic Introduction to PyObjC and Cocoa

Along with the release of Leopard (OS X 10.5) came the release of PyObjC 2.0 which made getting started with a PyObjC project a snap. However, a fair number of people have remarked that there is not a comprehensive up-to-date tutorial on getting started with PyObjC 2.0 on Leopard.

Having developed Cocoa applications using both Objective-C and PyObjC, it's my experience that PyObjC lets you build complex applications quickly, and wanted do a little bit to help make PyObjC more accessible to newcomers, whether they are newcomers to PyObjC or new to Cocoa development in general. This tutorial is my attempt to provide a comprehensive walkthrough to creating an application with PyObjC in XCode 3.0 and InterfaceBuilder 3.0, and to help you get to know this excellent tool.

Lets get started.

Our Project

Starting from a blank slate, it can be intimidating to get started with PyObjC. However, once you complete this tutorial you should be ready to dive into PyObjC projects of your own. We'll be building a desktop application that will display data retrieved from freebase.com using metaweb.py 1.

We'll have a textfield that allows users to specify their searches, and a table that will display the formatted results. We'll use an NSArrayController to greatly simplify our code (don't worry if NSArrayController doesn't mean anything to you, it'll be explained later in the tutorial). We'll also look at writing and reading files from the application's support directory (the directory where OS X coding guidelines ask you to store temporary and saved data for your application). Finally, the last step will be to look at programatically creating new windows in the application using a second nib (nibs will be explained later as well).

We'll touch on a wide range of issues, problems and solutions you'll run into when using PyObjC to develop real programs, without making any assumptions about existing knowledge of Cocoa, XCode or PyObjC. If you know simple Python, then you're ready to get started.

Two Principles to Remember

When just starting out with PyObjC there are a couple things that will help you get moving in the right direction:

  1. PyObjC is Python. PyObjC is Objective-C, too. People getting started with Django often have trouble doing things and ask questions like "How do I read in a file from disk?" or "How can I do threading?" A frequent community answer is Django is Python. When working with PyObjC, too, thats an important answer to keep in mind.

    Anything you can do in Python, you can do in PyObjC. If there is a great Python library to help power your app, use it! If you need to do some metaprogramming magic to simplify your program's logic, use it!

    Equally important is the second half of the principle: PyObjC is Objective-C. This is particularly important for Objective-C programmers who are more familiar with the Cocoa libraries than with Python libraries: you get to pick and choose from both. Do you want to use NSTimer to handle a recurring event? No problem. What about using an Obj-C selector or CoreData? Easy as slightly undercooked pie.

  2. Convert from Python to ObjC by replacing : with _. When working with PyObjC you'll be converting ObjC methods into Pythonic form a lot. This is a simple two step process. First, replace all semicolons (:) with underscores (_), and then add the arguments into the method definition like you normally would in Python.

    Lets try a few examples of how that is done. First, we'll start with a short one. This method definition in Objective-C:

    -(int)add: (int)a and: (int)b;
    

    Becomes this code in Python:

    def add_and_(self,a,b):
        # implementation here
    

    For our second conversion lets try something a bit longer.

    - (NSImage *)dragImageForRowsWithIndexes:(NSIndexSet *)dragRows
                                tableColumns:(NSArray *)tableColumns
                                       event:(NSEvent *)dragEvent
                                      offset:(NSPointPointer)dragImageOffset
    

    Is translated into Python as:

    def dragImageForRowsWithIndexes_tableColumns_event_offset_(self,dragRows,tableColumns,dragEvent,dragImageOffset):
        pass
    

    Well, you can see that this conversion can lead to some pretty ugly method names, but its simple enough to perform. Actually, XCode knows how to autocomplete method names for PyObjC, so you won't even need to do the conversions yourself unless its a method from a class that you've defined.

Cocoa Pieces & Patterns

As we get started, there are a few important ideas that reoccur in Cocoa we need to look at briefly. (Are you already familiar with Cocoa development? You should skip down to Starting the Project.)

  • Every application you make is an instance of NSApplication (or a subclass of NSApplication).

  • NSApplication has a delegate. Delegates are a pattern that occurs frequently in Cocoa, and allows you to customize and control behavior without subclassing the specific class (i.e. control an NSTableView without subclassing NSTableView).

    For example, the NSApplication delegate allows you to specify code to run when the application launches, handle opening the application with a specific file, and specify code to run before the application terminates. One of the first things we'll do is modify our application's delegate, so we'll talk more about that in a bit.

  • Each user on an OS X system has a folder at~/Library/Application Support/. Inside that folder, an application is allowed to create a folder to store data and settings. For example, Colloquy has a folder at ~/Library/Application Support/Colloquy/.

    You should always store user specific data there, and not inside the application itself (which is both tempting and possible, since .app applications are really folders, but interferes with the 'just drag it into the Application folder' distribution method for applications).

  • Cocoa classes frequently use the Model/View/Controller division of responsibilities, and your classes should too. Models contain data, views present data, and controllers handle interactions between the two. In smaller applications, however, it doesn't always pay to be religious about following MVC; brevity should not be considered a design flaw.

  • If you have played with Cocoa you know that many classes start with NS. For example, NSTableView, NSWorkspace and NSButton. Objective-C only has one namespace, and thus we create namespaces by prefixing letters.

    Many people use their (or their company's) initials for their prefix. At times I use WL as my prefix, but at other times I use a subset of the application's name. For example I was working on a lesson planner named StrictlyEducation, and I used SE as its prefix2.

  • InterfaceBuilder makes creating GUIs very easy, and stores those interfaces in .nib and .xib files (.nib is usually used for pre-Leopard interfaces, and .xib for projects created in Leopard). We'll look more at these later, but for now its enough to remember they contain GUI configurations.

Starting The Project

Now that we've fleshed out our background knowledge, time to start building our project.

Open XCode (you'll need to have installed it off of your OS X 10.5 install dvd).

Go up to the File menu and select the first option: New Project (or use Shift-Apple-n).

Select a Cocoa-Python Application and then hit Choose....

Creating a new Cocoa/Python project in XCode.

Name our new application MetaWindow, set the path to somewhere reasonable (I like to create mine in my ~/git/ directory), and then click Save.

Saving a new Cocoa/Python project as MetaWindow.

Now that our project has been created this is the perfect time to put it under version control. I prefer Git, but you won't regret using Mercurial or even SVN. Just use something.

In a sense this is an optional step, but it really should be an ingrained habit. If it isn't, now is the best time to start. Sometimes you're going to screw things up, and you won't remember what you did or how to fix it. Version control transforms that event from a catastrophe into a quick check of the documentation to remember the syntax for merging.

Stuff in A Default Project

Now that you've created the project, there are already a bunch of files in it. Lets take a few moments to figure out what all this stuff is.

Default files in XCode after creating a Cocoa/Python project.

  • First, there are a number of .framework files here. AppKit, Cocoa, CoreData, Foundation and Python. These are helpful libraries that are being linked to your application when you compile.

  • Next there are main.m and main.py. They are the core application files, and you won't be touching them often (MetaWindow_Prefix.pch is like that, but even more so: you won't ever touch it). The one exception is that you'll need to add a simple Python import to main.py for Python files in your app. For example, it currently has this line:

    import MetaWindowAppDelegate
    

    If you created another file named MWModel.py (MW are the initials for MetaWindow, the application, and is the namespace we'll be using for this app) , then you'd need to expand that part of main.py to look like this:

    import MetaWindowAppDelegate
    import MWModel
    
  • MainWindow.xib (English) is an InterfaceBuilder file, and contains data about the application's interface.

  • Info.plist contains settings for the application, and InfoPlist.strings (English) is an internationalization file, which is used for supporting applications with locale specific labels and menus.

  • MetaWindowAppDelegate.py is a normal Python file, which contains the class for our application's delegate.

  • MetaWindow.app is our application's shell. When you build the application everything will be stuffed inside of it, but at the moment it's just an inanimate skeleton.

Teaching Our Delegate Some Tricks.

The first thing we're going to do is flesh out MetaWindowAppDelegate.py. Go ahead and double click on it to open it. Right now it only contains a few lines:

from Foundation import *
from AppKit import *

class MetaWindowAppDelegate(NSObject):
    def applicationDidFinishLaunching_(self, sender):
        NSLog("Application did finish launching.")

But we're going to add a few helpful methods. First we're going to add a stub for doing something just before the application closes.

def applicationWillTerminate_(self,sender):
    NSLog("Application will terminate.")

This method, applicationWillTerminate_, is really helpful for saving data.

You've probably noticed that we're using NSLog() a lot. Its the most convenient way to log data and send yourself messages. While running the program you can see those messages in XCode's console window, and also by using Console.app.

Next, we'll add two methods to make it easy to access and create files in the application's support folder. applicationSupportFolder will return the path to MetaWindow's application support folder, and pathForFilename will return the path to a filename inside the application's support folder.

def applicationSupportFolder(self):
    paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory,NSUserDomainMask,True)
    basePath = (len(paths) > 0 and paths[0]) or NSTemporaryDirectory()
    fullPath = basePath.stringByAppendingPathComponent_("MetaWindow")
    if not os.path.exists(fullPath):
        os.mkdir(fullPath)
    return fullPath

def pathForFilename(self,filename):
    return self.applicationSupportFolder().stringByAppendingPathComponent_(filename)

Finally, a quick mention about why the app delegate is a great tool. You can easily access and use your application delegate from anywhere within your code:

app_delegate = NSApplication.sharedApplication().delegate()
my_database = app_delegate.pathForFilename("db.sqlite")

This makes it a great place for utility methods. (By the way, this would be a great time to add a commit to your version control.)

Stubbing Out a New File

Next we're going to create a new file which we'll use to manage the user interface components for our app. Go up to the File menu and click New File... (Apple-N).

Adding a Python NSObject subclass file to an XCode project.

Select Python NSObject subclass and then click next. Name it MWController.py and then click Finish.

Open up MWController.py and it'll look like this:

from Foundation import *

class MWController(NSObject):
    pass

Modify the imports to look like this:

import objc
from Foundation import *

The objc module provides access to a number of important utilities for creating PyObjC applications. Here we're going to take advantage of two of them: the @objc.IBAction decorator, and the objc.IBOutlet() function.

You must apply the @objc.IBAction decorator to any method that you want to be accessible in InterfaceBuilder. This means that anything you'll want a user interface component to activate (perhaps a wasClicked_ method called after a button is clicked, etc) should be decorated with @objc.IBAction. Beware, though, that all such actions must have at least one argument (and thus one underscore in their Python definition). For example you must write def myFunc_(self,sender) instead of def myFunc(self). The value of sender is supplied by Cocoa and will be the button (or instance of another class) that calls the action.

In the same trend, objc.IBOutlet() allows you to connect together models in a .xib file. This concept is difficult to understand before you've used the InterfaceBuilder, so we'll look at this in more depth later.

Now lets add a couple fields to the MWController class.

class MWController(NSObject):
    tableView = objc.IBOutlet()
    textField = objc.IBOutlet()
    results = []

And we'll add a stub method.

@objc.IBAction
def search_(self,sender):
    search_value = self.textField.stringValue()
    NSLog(u"Search: %s" % search_value)

As mentioned before, the search_ method will be called by a UI component, so it needs the @objc.IBAction decorator, and also must take the argument sender.

Finally, the last step to adding a Python file to an XCode project is to open up main.py and import the file under this line:

# import modules containing classes required to start application and load MainMenu.nib

Adding MWController, that portion of main.py will look like this:

# import modules containing classes required to start application and load MainMenu.nib
import MetaWindowAppDelegate
import MWController

Go ahead and save main.py and MWController.py. Next we'll setup MainMenu.xib.

Set Us Up The Xib

We're going to work with InterfaceBuilder now. IB is a really excellent tool for visually creating graphical user interfaces, but it also has a bit of a learning curve. You're probably going to be confused at some point. The first time I used IB I nearly gave up and was a hair away from abandoning the idea of developing for OS X. These days it's more like a treasured friend. You just need to give yourself some time to get used to it.

In the Groups & Files panel in the XCode project, click the arrow beside the Resources folder. Now double click on MainMenu.xib to open it.

Opening the MainMenu.xib file in an XCode project.

Now you'll see three windows:

  • MainMenu.xib (English) contains references to all the objects in your .xib file. You can click on any of those and then open the Inspector (Tool menu, then Inspector, or Shift-Apple-i) to modify their details. You can also click on some of them (like MainMenu or Window (Window)) to make that item front and key.

  • Window (Window) represents the main window for the application, which will pop up when you run the application. We'll be adding some items to it momentarily.

  • Library contains all the widgets and objects you can use for creating your user interface.

The first thing we want to do is add an NSTableView to our Window (Window) window (as long as Major Major Major approves). Go to the Library window and look around for it, and then drag it into Window (Window).

Adding a NSTableview to window in InterfaceBuilder.

Next drag an NSTextField and an NSButton to Window (Window) as well.

Adding an NSTextField and NSButton in InterfaceBuilder.

Now we need to resize the tableView, textField and button. To resize them, hover your cursor over the side you want to adjust and drag the corner or edge. Occasionally you'll notice dotted blue lines appearing in the window. Those help you follow Apple's Human Interface Guidlines, and also help you align objects with each other. Go ahead and rearrange everything to look like this:

Rearranged items in InterfaceBuilder window.

Double click on the button and rename it as Search.

Editing button's text in InterfaceBuilder.

Now we're going to setup the autosizing logic for the three items. To do this, first open the Inspector (Tool->Inspector or Shift-Apple-i), then click on the tableView, and select the third tab in the inspector.

In the Inspector's third tab, you'll see a portion labeled Autosizing with two components: on the left is a box with struts and arrows, and on the right is a box that keeps changing sizes. In the left box click so that there are six red items: a strut at the top,left,bottom and right of the box, and double-headed arrows going both vertical and horizontal within the box.

It should look like this:

Editing the autosizing logic for a table in InterfaceBuilder.

Now click on the textField. It should have the horizontal double-headed arrow, and the top,left, and right struts. The button should have the top and right struts, but no double-headed arrows.

The combinations of what works and what doesn't can require some trial and error, and the easiest way to test is to go to the File menu, and then go to Simulate Interface (Apple-R). That allows you to resize the window and check that everything is working correctly. Go ahead and give it a whirl.

We're almost done messing with the xib file, so you can take a quick sigh of relief. Just one more little thing.

Adding a MWController to MainMenu.xib

Part of the utility of .xib and .nib files comes from their ability to store serialized instances of a class. If you you look at the MainMenu.xib window in InterfaceBuilder, there are already a number of serialized instances present: the cube labeled MetaWindowAppDelegate is one, and so is Application.

This is useful because it allows you to graphically connect objects together. For example, click on Application and go to the fifth tab in the Inspector. There you can see that it has an outlet named delegate which has been connected to MetaWindowAppDelegate.

Now we want to create an instance of MWController and set up its outlets (and an action as well).

Go to the Library window and in the search field at the bottom type in NSObject (make sure you are selecting the Library folder in the outline view in the same window, or you may not find NSObject with your search).

Drag the NSObject icon over into your MainMenu.xib window. A new cube named Object should appear.

Adding an NSObject in InterfaceBuilder.

Click on Object and go to the sixth tab in the Inspector (the tab is named Object Identity). In the top text field, whose label is Class replace NSObject with MWController and hit enter.

Changing NSObject's class in InterfaceBuilder.

Now go back to the MainMenu.xib window and click on Controller (it's the object that was named Object just a few moments ago). Also get the window named Window in view so that you can see its tableView and textField.

Press and hold down the control key and drag--starting at Controller-- to the textField in Window. A little heads-up-display box will popup and ask you to select between textField and tableView; choose textField.

Do the same thing again, starting dragging at Controller and this time releasing on top of the tableView. When the popup appears select tableView.

Assigned outlets in InterfaceBuilder.

The last connection we're going to make will be from the Search button to Controller. For assigning actions you start with the input (here, the button) and drag to the object whose action you want to call (here, Controller).

Select the Search button and start holding control. Drag from the button to Controller and then release. A little black box with only one option, search:, will appear; select that option.

Save. Close InterfaceBuilder. Congratulate yourself for surviving.

Building And Verify

The last step in this first segment is to build our application and verify that what we have so far is working. To do that, go back to XCode and click on Build and Go (or go to the Build menu then Build and Go, or just use Apple-Return).

After a few seconds it will open open the window with our app. Huzzah.

Now--with the app still running--go back to XCode and click on the small icon that says gdb on it (or Shift-Apple-R). That will launch the debugging console for the app. With the console in sight, type something into the textField in the app, and then click the Search button.

A picture of a debug console window running under an app built in XCode.

We've successfully stubbed out our application. It doesn't do a whole lot yet, but we've covered a tremendous amount of material, and we have established a solid foundation to keep building on as we go.

Ending Part One

This concludes the first segment of this tutorial. You can continue on with the second segment here. You can download the current state of the project here. There is also a GitHub repository, which is the recommended way of accessing the code.

Have I made any grave mistakes? Thoughts about how to improve on things thus far? Ideas for additional topics to cover? Leave a comment!


  1. Freebase is a website similar to Wikipedia that provides easy access to structured data under several open licenses. Its pretty impressive, and well worth a few minutes of your time to check out.

  2. StrictlyEducation was my first extended project in pure Objective-C, taught me a tremendous amount about ObjC, and eventually sank into non-development when I felt like the market for such software would be non-existent.

All Rights Reserved, Will Larson 2007 - 2014.