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. We'll ask the class NSApplication to give us a
reference to the running application.
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">
Next, get a reference to the NSApplication class:
? (setf *my-app*
(let* ((class-name (%make-nsstring "NSApplication"))
(appclass (#_NSClassFromString class-name)))
(#/release class-name)
(#/sharedApplication appclass)))
#<LISP-APPLICATION <LispApplication: 0x1b8de0> (#x1B8DE0)>
Let's review this form step-by-step.
First of all, it's going to store the returned application
object in the variable *my-app*
, so that we have it
convenient for later use.
We need an NSString
object that contains the
name of the application class, so the code allocates one by
calling %make-nsstring
. The NSString
object is a dynamically-allocated foreign object, not managed by
Lisp's garbage-collector, so we'll have to be sure to release it
later.
The code next uses the class-name to get the
actual NSApplication
class object, by
calling #_NSClassFromString
.
Finally, after first releasing the NSString
object, it calls #/sharedApplication
to get the
running application object, which turns out to be an instance
of LispApplication
.
Voilą! We have a reference to the running Clozure CL
application object! Now we can ask it for its zone, where it
allocates objects in memory:
? (setf *my-zone* (#/zone *my-app*))
#<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: (@class 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:
(@class 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, and we can release them:
? (setf *nib-path* (#/release *nib-path*))
NIL
? (setf *my-dict* (#/release *my-dict*))
NIL
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-class-name (%make-nsstring "NSApplication"))
(app-class (#_NSClassFromString class-name))
(app (#/sharedApplication appclass))
(app-zone (#/zone app))
(nib-name (%make-nsstring (namestring nib-path)))
(dict (#/dictionaryWithObject:forKey:
(@class ns-mutable-dictionary) app #@"NSNibOwner")))
(#/loadNibFile:externalNameTable:withZone: (@class ns-bundle)
nib-name
dict
app-zone)))
The trouble with this function is that it leaks two strings
and a dictionary every time we call it. We need to release the
variables app-class-name
, nib-name
,
and dict
before returning. So how about this
version instead?
(defun load-nibfile (nib-path)
(let* ((app-class-name (%make-nsstring "NSApplication"))
(app-class (#_NSClassFromString class-name))
(app (#/sharedApplication appclass))
(app-zone (#/zone app))
(nib-name (%make-nsstring (namestring nib-path)))
(dict (#/dictionaryWithObject:forKey:
(@class ns-mutable-dictionary) app #@"NSNibOwner"))
(result (#/loadNibFile:externalNameTable:withZone: (@class ns-bundle)
nib-name
dict
app-zone)))
(#/release app-class-name)
(#/release nib-name)
(#/release dict)
result))
This version solves the leaking problem by binding the result
of the load call to result
, then releasing the
variables in question 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: (@class 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: (@class ns-mutable-dictionary)
app #@"NSNibOwner"
objects-array #&NSToplevelObjects))
...)
...)
We'll want to release the NSMutableArray
object before returning, but first we need 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.
(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-class-name (%make-nsstring "NSApplication"))
(app-class (#_NSClassFromString app-class-name))
(app (#/sharedApplication app-class))
(app-zone (#/zone app))
(nib-name (%make-nsstring (namestring nib-path)))
(objects-array (#/arrayWithCapacity: (@class ns-mutable-array) 16))
(dict (#/dictionaryWithObjectsAndKeys: (@class ns-mutable-dictionary)
app #@"NSNibOwner"
objects-array #&NSNibToplevelObjects))
(toplevel-objects (list))
(result (#/loadNibFile:externalNameTable:withZone: (@class ns-bundle)
nib-name
dict
app-zone)))
(dotimes (i (#/count objects-array))
(setf toplevel-objects
(cons (#/objectAtIndex: objects-array i)
toplevel-objects)))
(#/release app-class-name)
(#/release nib-name)
(#/release dict)
(#/release objects-array)
(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.