Description

This document walks you through creating a Cocoa application using Chicken and the objc egg. We'll develop an application called Currency Converter, based on Apple's Cocoa Application Tutorial. This tutorial will be referred to through this document, so open a copy now.

Download

cocoa-app.tgz: the completed sample application, as well as a Makefile template you'll need for this project.

Creating the Currency Converter Project

Open XCode and create a new Cocoa Application called Currency Converter in your desired Project Directory. I recommend that the Project Directory not contain spaces, to avoid problems with Emacs and run-scheme. It's fine if the Project Name contains spaces.

For this document, we will use ~/scheme/currency-converter as the Project Directory and Currency Converter as the Project Name.

Delete main.m from the project, as we don't need it and won't be using XCode to compile our source code.

Creating the Currency Converter Interface

This tutorial section is entirely free of code, so simply follow Apple's instructions to create the application's interface.

Defining the ConverterController Class

Also proceed through this tutorial section ...

Interconnecting the ConverterController Class and the User Interface

... and this one, as well.

Detour: Implementing and debugging ConverterController

At this point, the interface has been fleshed out enough to do some testing. Instead of defining the Converter (Model) class right away, we're going to create the ConverterController class and implement a simple convert: method, which simply displays its inputs to the terminal.

In Apple's tutorial, the class skeleton is written for you by XCode. In our case, we need to write it ourselves. In particular, the class outlets will be included as instance variables.

Create the Makefile

The build process used in this document relies on XCode to generate a skeleton application--that is, everything except for the executable--and a Makefile to build and insert the executable to complete the application.

First, copy the Makefile template from cocoa-app.tgz into your Project Directory:

 cp cocoa-app/Makefile.template ~/scheme/currency-converter/Makefile

You would normally customize this template to reflect your application's name (APPNAME), the name of the temporary binary built by Chicken (BINARY) and the actual build process for your executable. In this case, APPNAME is already set to Currency Converter, BINARY is converter and the build process simply compiles converter.scm to converter. So, you can use the Makefile as-is.

Write the program

Type in the following program and save it as converter.scm in ~/scheme/currency-converter. This program defines the ConverterController class and starts the application.

 (use objc cocoa)

 (define-objc-class ConverterController NSObject ((outlet: amountField)
                                                  (outlet: dollarField)
                                                  (outlet: rateField))
   (- VOID ((convert: ID sender))
      (print "convert: method called with sender " seder)  ;; typo!
      (print "Exchange rate: " (@ @rateField FloatValue))  ;; typo!
      (print "Dollars: " (@ @dollarField FloatValue))))    ;; typo!

 (ns:application-main)

This program contains three very deliberate typos.

Make the program

Type make. This will invoke XCode and generate a skeleton application in the usual XCode build area. In this case, that will be build/Debug/Currency Converter.app. Whenever you make a change to a Nib file or another resource using XCode, you should re-run make so your build directory will be updated.

make then builds the converter binary from converter.scm and copies it into the application bundle as the final executable name, Currency Converter.

Run the program from the Finder

Open the Finder and you will notice make has conveniently created an alias to the Currency Converter application in your Project Directory. Double-click it to run.

You'll notice the application starts up fine and accepts input, but unceremoniously disappears without warning when you click the Convert button. This is due to the typos in the convert: method. However, apps run from the Finder don't have a terminal to send errors to, and the Objective C bridge does not (currently) display an error box when a Scheme error occurs. Let's see how to debug this problem, then.

Run the program from the Terminal

make, it turns out, has created a shell script called runapp in ~/scheme/currency-converter. When run from a terminal, runapp invokes the application with TTY (terminal) input and output. Go ahead and run it now, and click on Convert.

 Error: unbound variable: seder

Now you could fix this typo, run make again, and crash again on the next two typos. But there is an easier way.

Build an interpreted version

Typemake interpret to build an interpreted version of Currency Converter. Instead of generating a binary, it creates a symlink to csi. This grants csi access to the application bundle's resources.

runapp, in this case, will invoke the interpreter with any arguments you pass. At startup it switches to the directory runapp resides in. You can then load your code and start the application's event loop.

Start the interpreter, load converter.scm and click the Convert button:

 $ ./runapp -quiet
 #;1> (load "converter.scm")    
 Error: unbound variable: seder
 #;1>

Now resume the Cocoa application:

 #;1> (cocoa:run)

As we haven't changed the code, we'll crash again as soon as we hit Convert. We could have retyped the class definition into the interpreter session, but interactive development is a job for Emacs.

Application as Inferior Scheme

In Emacs, load up converter.scm into a buffer. Start an new Scheme session with M-x run-scheme and, for the interpreter path, give the path to runapp (~/scheme/currency-converter/runapp). Then start the application and click Convert once again:

 #;1> (load "converter.scm")    
 Error: unbound variable: seder
 #;1>

Now change seder to sender, send the new class definition to the running interpreter with (for example) C-x C-e, and resume the application:

 #;1> Warning: (define-objc-class): class already registered: ConverterController
 #;2> (cocoa:run)

When you click Convert this time, we get a little farther.

 #;2> (cocoa:run)
 convert: method called with sender #<objc-instance <NSButton: 0x11650b0>>
 Error: (objc:invoker) method not found: "FloatValue"
 #;2>

Indeed, we have miscapitalized the method name. The correct message is floatValue, not FloatValue. Fix this (and the subsequent line if you like), send the class definition and resume the application. A correct run will look similar to this:

 #;2> Warning: (define-objc-class): class already registered: ConverterController
 #;3> (cocoa:run)
 convert: method called with sender #<objc-instance <NSButton: 0x11650b0>>
 Exchange rate: 0.800000011920929
 Dollars: 38.0

Defining the Converter Class

It would be quite easy to perform the currency conversion computation directly in ConverterController, or to call a Scheme function to do so. However, we'll follow along with the tutorial and create a Converter class whose instances can do the conversion.

So, return to the tutorial and create and instantiate the Converter class in Interface Builder.

Implementing Currency Converter's Classes

We can skip Generate the Source Files and Place the Implementation Files in the Appropriate Group, since we're not using XCode to handle the Scheme source. We'll proceed to implement Converter and finish ConverterController.

First, define the Converter class in converter.scm and implement its convertCurrency:atRate: method. To aid readability, from this point on we will define and call methods using hyphenated selectors, which separate words with hyphens rather than capital letters.

 (define-objc-class Converter NSObject ()
   (- FLT ((convert-currency: FLT currency)   ; hyphenated form of convertCurrency:
           (at-rate: FLT rate))               ; hyphenated form of atRate:
      (* currency rate)))

Second, you'll notice that ConverterController needs to communicate with the Converter instance. The tutorial had us define a converter outlet earlier in Interface Builder, but we didn't add it to our class definition. Add the converter outlet to the instance variables in the definition of ConverterController now.

Finally, we'll redefine ConverterController's convert: method by translating the tutorial's Objective C implementation directly into Scheme.

The final program should look something like this:

 (use objc cocoa)

 (define-objc-class ConverterController NSObject ((outlet: amountField)
                                                  (outlet: dollarField)
                                                  (outlet: rateField)
                                                  (outlet: converter))
   (- VOID ((convert: ID sender))
      (let* ((currency (@ @dollarField float-value))
             (rate     (@ @rateField   float-value))
             (amount   (@ @converter   convert-currency: currency
                                       at-rate: rate)))
        (@ @amountField set-float-value: amount)
        (@ @rateField select-text: self))))

 (define-objc-class Converter NSObject ()
   (- FLT ((convert-currency: FLT currency)
           (at-rate:          FLT rate))
      (* currency rate)))

 (ns:application-main)

If you're using the interpreter, you won't need to run make again at all, unless you need to modify your nib file. Remember, any time you make changes to resources handled by XCode, run make to propagate these changes into your build.

A winner is you!

You've completed your first Cocoa application in Chicken. Now, you might try adding other features. For example, since we aren't running under a debugger, we need to throw an error to get back to the REPL. To do this in a controlled manner, try defining a Debug->Break menu item which sends a break: message to a Debugger instance. You'll have to instantiate and write the appropriate Debugger class yourself.

Good luck!