Nib-Loading HOWTO

This HOWTO shows how you can load nibfiles into a running copy of Clozure CL by evaluating Lisp forms. You might want to load nibfiles this way to test user-interface elements that you are working on for an application project, or to enable an application to dynamically load optional user-interface elements.

Nibfiles

A large part of developing Cocoa applications is creating user-interface elements using the Cocoa frameworks. Although it's perfectly possible to create any user-interface element just by making method calls against the frameworks, the more standard way to design a user interface is to use Apple's InterfaceBuilder application to create nibfiles—files of archived Objective-C objects that implement the user-interface elements.

InterfaceBuilder is an application that ships with Apple's Developer Tools. The Developer Tools are an optional install that comes with Mac OS X. Before you can use this HOWTO, you'll need to make sure that Apple's Developer Tools are installed on your system. Members of Apple's developer programs may download the tools for free from Apple's developer website, but normally there is no need. You can simply use the optional Developer Tools installer on the Mac OS X system disks to install the tools.

Using Nibfiles

Using InterfaceBuilder, you can quickly and easily create windows, dialog boxes, text fields, buttons, and other user-interface elements. The elements you create with InterfaceBuilder have the standard appearance and behavior specified by Apple's Human Interface Guidelines.

InterfaceBuilder saves descriptions of these objects in nibfiles. These files contain archived representations of Objective-C classes and objects. When you launch an application and it loads a nibfile, the Cocoa runtime creates these Objective-C objects in memory, complete with any instance-variable references to other objects that might have been saved in the nibfile. In short, a nibfile is an archived collection of user-interface objects that Cocoa can quickly and easily revive in memory.

The normal way that Objective-C programmers use nibfiles is by storing them in an application bundle. The application's Info.plist file (also stored in the bundle) specifies which nibfile is the application's main nibfile, and that file is loaded automatically when the application starts up. The application can dynamically load other nibfiles from the bundle by making method calls.

Lisp applications written with Clozure CL can also use nibfiles in this same fashion (see the "currency-converter" HOWTO in the "cocoa" examples folder), but Lisp programmers are accustomed to highly interactive development, and might want to simply load an arbitrary nibfile into a running Clozure CL session. Fortunately, this is easy to do.

How To Load a Nibfile

Let's start by loading a very simple nibfile from the Clozure CL Listener window. Start by launching the Clozure CL application.

In the same directory as this HOWTO file, you'll find a nibfile named "hello.nib". This is an extremely simple nibfile that creates a single Cocoa window with a greeting in it. We'll use forms typed into the Listener window to load it.

We're going to call the Objective-C class method loadNibFile:externalNameTable:withZone: to load the nibfile into memory, creating the window that is described in the file. First, though, we need to set up some data structures that we'll pass to this method.

The arguments to loadNibFile:externalNameTable:withZone: are a pathname, a dictionary object, and a memory zone. As with every Objective-C method call, we also pass the object that receives the message, which in this case is the class NSBundle.

The pathname is just a reference to the nibfile we want to load. The dictionary holds references to objects. In this first simple example, we'll use it only to identify the nibfile's owner, which in this case is the application itself. The zone is a reference to the area of memory where the nibfile objects will be allocated.

Don't worry if none of this makes sense to you; the code to create these objects is simple and straightforward, and should help clarify what's going on.

1. Get the Zone

First, we'll get a memory zone. We'll tell Cocoa to allocate the nibfile objects in the same zone that the application uses, so getting a zone is a simple matter of asking the application for the one it's using.

Before we can ask the application anything, we need a reference to it. When the Clozure CL application starts up, it stores a reference to the Cocoa application object into the special variable *NSApp*.

Start by changing to the CCL package; most of the utility functions we'll use are defined in that package:

        ? (in-package :ccl)
        #<Package "CCL">
      

We have a reference to the running Clozure CL application object in the special variable *NSApp*. We can ask it for its zone, where it allocates objects in memory:

        ? (setf *my-zone* (#/zone *NSApp*))
        #<A Foreign Pointer #x8B000>
      

Now we have a reference to the application's zone, which is one of the parameters we need to pass to loadNibFile:externalNameTable:withZone:.

2. Make a Dictionary

The dictionary argument to loadNibFile:externalNameTable:withZone: is used for two purposes: to identify the nibfile's owner, and to collect toplevel objects.

The nibfile's owner becomes the owner of all the toplevel objects created when the nibfile is loaded, objects such as windows, buttons, and so on. A nibfile's owner manages the objects created when the nibfile is loaded, and provides a way for your code to get references to those objects. You supply an owner object in the dictionary, under the key "NSNibOwner".

The toplevel objects are objects, such as windows, that are created when the nibfile is loaded. To collect these, you can pass an NSMutableArray object under the key NSNibTopLevelObjects.

For this first example, we'll pass an owner object (the application object), but we don't need to collect toplevel objects, so we'll omit the NSNibTopLevelObjects key.

        ? (setf *my-dict* 
        (#/dictionaryWithObject:forKey: ns:ns-mutable-dictionary
        *my-app* 
        #@"NSNibOwner"))
        #<NS-MUTABLE-DICTIONARY {
        NSNibOwner = <LispApplication: 0x1b8e10>;
        } (#x137F3DD0)>
        
      

3. Load the Nibfile

Now that we have the zone and the dictionary we need, we can load the nibfile. We just need to create an NSString with the proper pathname first:

        ? (setf *nib-path* 
        (%make-nsstring 
        (namestring "/usr/local/openmcl/ccl/examples/cocoa/nib-loading/hello.nib")))
        #<NS-MUTABLE-STRING "/usr/local/openmcl/ccl/examples/cocoa/nib-loading/hello.nib" (#x13902C10)>
      

Now we can actually load the nibfile, passing the method the objects we've created:

        ? (#/loadNibFile:externalNameTable:withZone: 
        ns:ns-bundle
        *nib-path*
        *my-dict*
        *my-zone*)
        T
      

The window defined in the "hello.nib" file should appear on the screen. The loadNibFile:externalNameTable:withZone: method returns T to indicate it loaded the nibfile successfully; if it had failed, it would have returned NIL.

At this point we no longer need the pathname and dictionary objects. The *nib-path* we must release:

        ? (setf *nib-path* (#/release *nib-path*))
        NIL
      

The *my-dict* instance was not created with #/alloc (or with MAKE-INSTANCE), so it is already autoreleased, and we don't need to release it again.

Making a Nib-loading Function

Loading a nibfile seems like something we might want to do repeatedly, and so it makes sense to make it as easy as possible to do. Let's make a single function we can call to load a nib as needed.

The nib-loading function can take the file to be loaded as a parameter, and then perform the sequence of steps covered in the previous section. If we just literally do that, the result will look something like this:

(defun load-nibfile (nib-path)
  (let* ((app-zone (#/zone *NSApp*))
         (nib-name (%make-nsstring (namestring nib-path)))
         (dict (#/dictionaryWithObject:forKey: 
                 ns-mutable-dictionary app #@"NSNibOwner")))
    (#/loadNibFile:externalNameTable:withZone: ns:ns-bundle
                                               nib-name
                                               dict
                                               app-zone
                                             )))
      

The trouble with this function is that it leaks a string every time we call it. We need to release the nib-name before returning. So how about this version instead?

(defun load-nibfile (nib-path)
  (let* ((app-zone (#/zone *NSApp*))
         (nib-name (%make-nsstring (namestring nib-path)))
         (dict (#/dictionaryWithObject:forKey: 
                ns-mutable-dictionary app #@"NSNibOwner"))
         (result (#/loadNibFile:externalNameTable:withZone: ns:ns-bundle
                                                            nib-name
                                                            dict
                                                            app-zone)))
    (#/release nib-name)
    result))
      

This version solves the leaking problem by binding the result of the load call to result, then releasing the nib-name before returning the result of the load.

There's just one more problem: what if we want to use the dictionary to collect the nibfile's toplevel objects, so that we can get access to them from our code? We'll need another version of our function.

In order to collect toplevel objects, we'll want to pass an NSMutableArray object in the dictionary, stored under the key NSNibTopLevelObjects. So we first need to create such an array object in the let form:

(let* (...
       (objects-array (#/arrayWithCapacity: ns:ns-mutable-array 16))
       ...)
  ...)
      

Now that we have the array in which to store the nibfile's toplevel objects, we need to change the code that creates the dictionary, so that it contains not only the owner object, but also the array we just created:

  (let* (...
         (dict (#/dictionaryWithObjectsAndKeys: ns:ns-mutable-dictionary
                    app #@"NSNibOwner"
                    objects-array #&NSToplevelObjects
                    +null-ptr+))
         ...)
    ...)
  
      

We now want to collect the objects in it. We'll do that by making a local variable to store them, then iterating over the array object to get them all. (Normally, when we want to keep an object from an array, we have to retain it. Top-level nib objects are a special case: they are created by the nib loading process with a retain count of 1, and we are responsible for releasing them when we're through with them.)

  (let* (...
         (toplevel-objects (list))
         ...)
    (dotimes (i (#/count objects-array))
      (setf toplevel-objects 
            (cons (#/objectAtIndex: objects-array i)
                  toplevel-objects)))
    ...)
      

After collecting the objects, we can release the array, then return the list of objects. It's still possible we might want to know whether the load succeeded, so we use values to return both the toplevel objects and the success or failure of the load.

The final version of the nib-loading code looks like this:

(defun load-nibfile (nib-path)
  (let* ((app-zone (#/zone *NSApp*))
         (nib-name (%make-nsstring (namestring nib-path)))
         (objects-array (#/arrayWithCapacity: ns:ns-mutable-array 16))
         (dict (#/dictionaryWithObjectsAndKeys: ns:ns-mutable-dictionary
                    *NSApp* #@"NSNibOwner"
                    objects-array #&NSNibTopLevelObjects
		    +null-ptr+))
         (toplevel-objects (list))
         (result (#/loadNibFile:externalNameTable:withZone: ns:ns-bundle
                                                            nib-name
                                                            dict
                                                            app-zone)))
    (dotimes (i (#/count objects-array))
      (setf toplevel-objects 
            (cons (#/objectAtIndex: objects-array i)
                  toplevel-objects)))
    (#/release nib-name)
    (values toplevel-objects result)))
      

Now we can call this function with some suitable nibfile, such as simple "hello.nib" that comes with this HOWTO:

? (ccl::load-nibfile "hello.nib")
(#<LISP-APPLICATION <LispApplication: 0x1b8da0> (#x1B8DA0)>
 #<NS-WINDOW <NSWindow: 0x171344d0> (#x171344D0)>)
T

      

The "Hello!" window appears on the screen, and two values are returned. The first value is the list of toplevel objects that were loaded. The second value, T indicates that the nibfile was loaded successfully.

What About Unloading Nibfiles?

Cocoa provides no general nibfile-unloading API. Instead, if you want to unload a nib, the accepted approach is to close all the windows associated with a nibfile and release all the toplevel objects. This is one reason that you might want to use the "NSNibTopLevelObjects" key with the dictionary object that you pass to loadNibFile:externalNameTable:withZone:—to obtain a collection of toplevel objects that you release when the nibfile is no longer needed.

In document-based Cocoa applications, the main nibfile is usually owned by the application object, and is never unloaded while the application runs. Auxliliary nibfiles are normally owned by controller objects, usually instances of NSWindowController subclasses. When you use NSWindowController objects to load nibfiles, they take responsibility for loading and unloading nibfile objects.

When you're experimenting interactively with nibfile loading, you may not start out by creating NSWindowController objects to load nibfiles, and so you may need to do more of the object management yourself. On the one hand, loading nibfiles by hand is not likely to be the source of major application problems. On the other hand, if you experiment with nib-loading for a long time in an interactive session, it's possible that you'll end up with numerous discarded objects cluttering memory, along with various references to live and possibly released objects. Keep this in mind when using the Listener to explore Cocoa. You can always restore your Lisp system to a clean state by restarting it, but of course you then lose whatever state you have built up in your explorations. It's often a good idea to work from a text file rather than directly in the Listener, so that you have a record of the experimenting you've done. That way, if you need to start fresh (or if you accidentally cause the application to crash), you don't lose all the information you gained.