Well, after quite a bit of reading and trying to puzzle out how to use Interface Builder to work on applications using Cocoa Bindings, I've concluded that I don't understand well enough. This is rather surprising to me, since I have more than twenty years of development experience with more than fifteen years of object-oriented and GUI programming experience. But whether I'm in the early stages of dementia or Apple's documentation leaves something to be desired, I clearly have to take a different approach :-). So, I've gone back to the
Currency Converter tutorial to try to make a few things gel.
I know that Xcode 3.0 and a brand spanking new version of Interface Builder are due out with Leopard sometime this spring (of 2007), but I'm trying to learn as much as I can before then. After the new Xcode and Interface Builder come out, I may revisit this subject to weigh in on the dementia versus documentation debate.
I should point out that this article is not meant to be a slam of Xcode & Interface Builder. I don't have enough knowledge to know if it's my ignorance that is the issue, if Xcode & Interface Builder need improvements, or (probably most likely) some mix of the two. So, this is as much notes to myself for future reference as anything else, posted somewhere that others might find it helpful.
So, time to get started. Happily, the tutorial is pretty up-to-date:
You'd better be up to date[Note: This is a discussion of the tutorial and in no way takes the place of the tutorial. You probably need to have worked through
the tutorial or work on it in in parallel.]
We are going to create a simple currency converter, to understand how Cocoa implements the
MVC (Model/View/Controller) design pattern. The application itself is trivial:
The GUI for the Currency Converter Tutorial ApplicationAfter running the Xcode application and creating a "Cocoa Application" project, we see this:
The Main Xcode WindowWe are instructed to open the NIB file, named
MainMenu.nib, which is the file that contains the user interface. The user interface is edited using Interface Builder, Apple's sort-of WYSIWYG GUI builder. If we double-click on
MainMenu.nib, Interface Builder is launched. The tutorial walks us through a number of user interface tasks like renaming menus and menu items, titling the window, creating user interface elements, etc. All of these are simply until we get to creating some bindings.
Even the first binding action, setting up the tab order of the text items is reasonably straightforward. We control-drag from the starting field to the next field. This brings up the object inspectors
Connections tab where we choose
nextKeyView.
Specifying Tab Order of Text FieldsThe strange part, at least to me, is how we specify the first text field to receive focus. In the object window of the NIB, we see this:
There's the item named "First Responder" staring us in the face. But we don't touch it. Instead, we control-drag from the main window to the first text field to recieve keyboard input and choose
initialFirstResponder:
Setting the Main Window's "initial responder"This is non-obvious step number one.Next, you have to remember you need a custom controller and how to create one. I had no problem with this when I went through the tutorial because it gave me the steps. But when I went to write my own application, I forgot that you have to switch to the
Classes tab of the NIB window, select the class you want to sub-class (
NSObject in this case), and hit ente r. As it turns out, you can also use the contextual menu to create the sub-class:
Regardless of whether you use the menu (remembering it exists) or hit enter, this is only obvious with practice, giving us
non-obvious step number two.
For a complete newbie like myself, knowing what to sub-class is non-obvious, leading to
non-obvious step number three. The tutorial tells us to sub-class
NSObject, so we do so, naming the new class
ConverterController.
Creating the ConverterController classThe tutorial now goes on to talk about
outlets. When they are first brought up, their purpose and the reason for giving them a unique name is not obvious. After reading through this again, it now seems clear that outlets are really just object references. They are marked in the code with the
IOOutlet macro, a null macro apparently there solely to let Interface builder find them in the code. But things get more confusing quickly. The next section telegraphs the murkiness to come. The first sentence in the "Target/Action in Interface Builder" section says:
"You can view (and complete) target/action connections in the Connections pane in the Interface Builder inspector. This pane is easy to use, but the relation of target and action in it might not be apparent."After introducing the new term
"target/action connections", in the very next sentence they define a
target as the
outlet of a
cell that sends an
action message. What the heck is a cell and what is an action message? The next few paragraphs weren't particularly helpful in explaining this.
After some additional reading and head-scratching, I think it's much simpler than they made it. UI components like buttons have their own object references ("outlets") that refer to things, and they use those references to invoke methods ("actions") on the object they refer to. It's a stinkin' object reference used to invoke a method. I honestly still have no idea what a "cell" is, but I'm betting it has to do with some abstract pattern of the UI components of the Cocoa GUI frameworks that I really don't need to know about at this point. There is one subtle bit of information buried in here: "actions" follow a naming pattern (again, to let Interface Builder find them). I wish they'd just told us that from the outset.
So now I think I finally get it:
start your control-drag from the object that will do the invoking and stop on the thing to be invoked. So if your button needs to invoke the controller because it was clicked, drag from the button to the controller. If your controller needs to alter the model object, drag from the controller to the model object.
Along the way, another non-obvious thing showed up: when you view the instances tab of the NIB window, you don't see the outlets and actions of that instances class. For example, the
attributes tab of the inspector is empty:
Inspector Display when Examining InstancesThen, when you switch to the
Classes tab and have ConverterController selected, you see the outlets and actions:
Inspector Display when Examining ClassesDon't get me wrong: this is good contextual behavior by the Inspector. But for a newbie it was a revelation when I first noticed that flipping to the
Classes tab (where the
ConverterController class was already selected) showed the outlets and actions I'd defined a few minutes ago and lost track of. I think a lot of this is due to trying to absorb all the new terms in the first place, leaving me dazed and confused...
The next several pages of the tutorial are reasonably straightforward, describing how to add required methods and providing the code for those methods. There's a fair bit of Objective-C, but nothing shocking to someone who understands OO.
After some housekeeping and doing a build, I was able to run my application.
And of course it didn't work.Prior to taking the time to write this down, I would have been at a loss at this point. But by forcing myself to (mostly?) sort out the whole "outlet"/"action"/"connection" business, I had enough information that I quickly figured out I'd somehow deleted the connection from my ConverterController to my Converter. Once I reconnected them, the application started working.
Getting zero feedback when I clicked and the text window for the run produce is a real issue. I understand the need to not flood a console with messages in a real application. And I realize a production GUI application may have nowhere to send messages. But Xcode has a window with text output for the session when you run the application:
The Run Log Window of XcodeIt only seems natural, especially when running the debug version, to print some error messages indicating where my problem manifested. I hope someone can tell me a simple way to turn on what should have been on by default.