After setting up MacRuby as described in our previous blog post and having set up Xcode, it is now time to create our very first Mac application in Ruby. In this series of articles, we assume that you have the latest version of OS X installed on your Mac. By the time of this writing, that is Snow Leopard and it is important to emphasize this seeing as some Cocoa API methods have been deprecated or added since its last iteration. Without further ado, allons-y!
Creating a new project
After starting up Xcode:
- Select File
- Select New Project
A dialog window as shown below should appear.
From this window
- Select MacRuby application
- Click on Choose…
A file save dialog should now appear as displayed below.
In this window:
- Type in BornToBeAlive as our project name.
- Click on Save.
A new window should now appear representing the newly created project in the Xcode environment.
By the time of this writing our environment is setup to target OS X 10.5 (Leopard). As mentioned before, we’re going to target OS X 10.6 (Snow Leopard) seeing as we want to utilize the most mature Cocoa API and platform. As such we need to set this target environment to OS X 10.6 in the overview popup menu.
Now that that’s been dealt with, we can finally really start working on our application.
Designing our Interface
Before we’re going to write even a single line of code, let’s first start off by designing the user interface to our application. To do this, we first need to discuss how our programming environment deals with this kind of information.
Certain information about our interfaces, in particular the one’s we’ve created using Interface Builder (the UI design application of Xcode) are usually stored in bundles called NIB files.
As of Xcode 3, these are actually stored in files with the xib file extension as opposed to the nib file extension. Worry not however, as they are just different formats for storing the same information: where xib files appear to be represented as XML text files, a nib file is an archive. More importantly however, the difference between xib and nib files are that the former will be compiled within the executable and will not appear in the final .app bundle.
In general, Xcode will ultimately know what to do with these files and how to treat them. It is for this reason that we’ll refer to both formats as nib files and basically assume no distinction between them.
In the Groups & Files panel, expand the NIB Files group.
In this panel
- Double click on MainMenu.xib
This action should result in Interface Builder firing up for this MainMenu.xib file. Various windows of Interface Builder should now be visible to you.
In the MainMenu.xib window:
You see a collection of objects and proxy objects that are necessary for describing our user interface. The objects displayed in this window are objects that describe our application’s user interface and are stored in a freeze dried manner, i.e. serialized. Upon loading the NIB file, they get thawed and instantiated again.
In light of this, it is considered good practice to store only one window object per nib file. This way, we only load in the windows when we really need them (thus conserving memory) and will keep our design more maintainable too as each window and its components will be contained by its own NIB file. The latter will allow us to possibly recycle NIB files too in other projects.
Strictly speaking, we also shouldn’t store a window in our MainMenu.xib as is now the case, but in order to load this in, we require a notion of delegates. The latter is something we’ll discuss in a later article so for now, we’ll keep the window object in MainMenu.xib.
For now:
- Double click on Window
This should put the focus on our initial window and via the inspector, we can edit some of its properties, such as the title of the window instance.
While having selected the Window in the MainMenu.xib window, in the inspector window:
- Select the first tab, so that the inspector window is displaying the Window Attributes.
- Edit the Title field to Born to be Alive
- Hit the enter key
Your window should now bare the title Born to be Alive. How exciting!
Let’s add a Label to this window proclaim this message of life even bigger. To add cocoa components to our window, we need to drag and drop them from our Library panel to the window we want to add them to.
In the Library panel:
- Set the focus on the search field at the bottom of the Library panel.
- Type in Label. Notice that as we type character per character, the components get filtered based on our query.
Your Library panel should now display a few options for labels and should look something like the following:
Note that my Library panel will most likely display more options here in terms of labels as I’ve also installed the BWToolkit palette too. We won’t be using any of those components for our MacRuby applications though, so it’s safe for you to ignore them for now.
Now it’s time to actually add the Label component to our window. As mentioned earlier on, we do this by simply dragging and dropping the Label icon from our Library panel to the window we want to add it to. Doing so should result in something similar to the following:
Great! Let’s rename the newly added Label to something more fitting. In the window displayed above:
- Double click on the newly added Label. This should now open up the ability to edit value of this Label.
- Type in Born
- Hit the enter key.
Your window should now look something like this:
Wouldn’t it be fun if we would have a button that we could press to unveil the full message of life? Let’s do just that by adding a button to the window we’re designing.
In the Library panel:
- Type Button in the search box at the bottom of the Library panel. In a similar fashion as the label, it should filter away the components that do not match the Button search description.
Whoah, so many buttons! Let’s just stick with the first shiny one for now.
- Drag and drop the first button that appears in the filtered selection to our window. Depending on where you dragged and dropped the button to, your window should now look something like this:
- Double click on the newly added button to be able to edit its title.
- Type in Unveil full message
- Hit the enter key.
Your window should now be shaping up pretty nicely and look something like:
We can actually build this project at this stage and see what we’ve just designed in the form of a real application! Indeed, the standard MacRuby cocoa template has set up most of the boilerplate to get us started. At a later stage, we’ll dive into the details of the bootstrapping process, but for now let’s just assume that it set it up all correctly and save our changes by selecting File->Save from the Interface Builder main menu.
Compiling is not the task of Interface Builder, but the task of Xcode so let’s switch back to Xcode. In the project window, click on the Build and Run button in its toolbar.
After compiling for a few seconds and linking all the dependencies, your new application should launch in your OS X dock, and in particular, the window you just designed should show up.
Clicking on the Unveil full message button doesn’t do a lot though, but that should come as no surprise seeing as we’ve only been designing the interface up to this point. We haven’t yet specified what should happen if one presses the Unveil full message button button.
In terms of Model-View-Controller (MVC) (a separation of concerns design pattern Cocoa relies heavily on), we need a controller to handle this kind of user input.
Writing our very first controller in MacRuby
As mentioned in the previous section, Cocoa was designed with the MVC paradigm in mind. Within this paradigm, we need some kind of controller layer to process the input provided by the user. This input ranges from keyboard events to mouse events. For our intents and purposes, we’re interested in handling mouse events on our Unveil full message button. In order to be able to do that, we need to create a controller that will be able to deal with that.
Also, we may want to process events employed by the user onto the window itself. This would normally require a NSWindowController instance. Taking in mind we want to process events from components within the window as well, we could choose to create a separate controller for these components as well, but that would likely make things overly granular, especially for the simple scenario we’re currently dealing with.
So in place of the latter, we can also just choose to consolidate the ability to process the events of a window and its components all together into a subclass of NSWindowController. This allows us to handle the general case of controlling an NSWindow as well as specify our own actions for handling the components contained by our NSWindow.
To do this, in the Groups & Files panel, expand the BornToBeAlive project as displayed below by clicking on the triangle next to it.
In this expanded project outline view:
- Right click (or ctrl+click) on Classes
- Select Add->New File
A new window should now appear giving you the options of what kind of new file you would like to add.
- From the Ruby user templates, select Ruby File.
- Click on Next
This will open up a Save File dialog.
- Type in MyWindowController.rb as the name for the new Ruby file.
- Click on Finish.
Your window should now look something like this:
As decided upon earlier, we now need to implement our MyWindowController class in a consolidated way such that it can control both the window and the elements contained by that window. We will achieve this by implementing MyWindowController as being a subclass of NSWindowController, and in doing so, specialize the NSWindowController class for our window.
In the code editor, type in:
class MyWindowController < NSWindowController
end
Tres bien! But how do we access the label to modify its value after a user has clicked on the Unveil full message button?
Well, according to the Apple cocoa documentation on NSWindowController, it is capable hold a reference to the window it controls/manages. Instinctively, we assume that through this reference, we could access the elements of the window.
Even though this is kind of true, the NSWindow class was designed for general cases as well and not just our specific window. This means that its design is well composed and that it will be rather tedious to acquire a reference to the element of the window we’re interested in manipulating. Most likely, it will involve a lot of chaining query calls which violates the principle of least knowledge.
To drive the point home, accessing an element in such a fashion in general will look something like: window.view.subviews[0]
A non-food solution to this would be to subclass our NSWindow to hold a direct reference to the elements we’re interested in manipulating, but that would require an assumption in terms of type seeing as our NSWindowController only deals with NSWindow and not OurSpecificWindow.
Taking all this into account, it’s probably just better to give our MyWindowController a direct reference to the element of the window we’re interested in manipulating.
In our particular case, we’d like to change the label’s string value after the user has clicked on the Unveil full message button to show the true message of life. This means we need a reference to the label within our MyWindowController and as such our code listing of MyWindowController now is:
class MyWindowController < NSWindowController
attr_accessor :my_label
end
In Cocoa, such a reference is usually referred to as an outlet, in our case, representing the metaphor of “being able to plug a label object into the outlet”. C’est bon indeed!
We now need to define the action to be taken when a user has clicked on our Unveil full message button. You might be wondering right now whether or not we also need a reference to the button from within MyWindowController as was the case for the label. This however, is not necessarily the case for us seeing as the button — an instance of NSButton — is an action emitter. More specifically, an NSButton inherits from NSControl and NSResponder allowing it to receive user input (e.g. mouse events) and convert these to action events and dispatch the latter to our code. We may touch base on this subject in more detail in a later article, but for now, suffice it to say that we don’t need such a property reference.
When the button receives a mouse event from the user that is of our interest, i.e. a click event, it will convert this event into a click action and try to dispatch the action message to our code. To that end, the button expects us to provide an object with an action handler method to handle the action on delivery. In Cocoa terminology, this object containing the action handler method is referred to as the target of the button.
An action handler method in MacRuby has a distinct method signature: it is a method with one parameter named sender. It is important to adhere to this rule for action handler methods as Xcode and Interface Builder will only then recognize them as being actions.
Let’s open up our code editor in Xcode for MyWindowController.rb and change the listing to:
class MyWindowController < NSWindowController
attr_accessor :my_label
def unveil_full_message_clicked(sender)
@my_label.stringValue = "42"
end
end
Here, we’ve added the unveil_full_message_clicked action, which will change the stringValue property of our label to the true message of life. Excellent! We’ve now set up all our code, and what remains now is to tell our button to who it should dispatch its action to.
We can setup this target-action for our button programmatic, but Interface Builder provides us with a visual solution for setting these up as well. For the sake of clarity and convenience, the visual solution that we’ll go over with in a few moments enjoys our preference for now.
Hooking our MyWindowController up to our window
In the previous sections we’ve gone from designing our window to providing an implementation for our MyWindowController class. They still need to be hooked up to one another however as they are now still disjoint entities that don’t know of eachothers existence. In this section, we’ll describe how we’ll be able to hook these things up visually using Interface Builder.
In Xcode:
- Double click on MainMenu.xib to open up Interface Builder for this nib file.
As we’ve discussed earlier, objects are stored in a freeze dried manner in our NIB file that get “thawed for use” when the NIB file is loaded again. In this case, we need an instance of MyWindowController to hook it up to our freeze dried window instance.
In Interface Builder in the Library Panel:
- Type in NSObject in the search field.
- Drag and drop the Object from the panel to the MainMenu.xib window of Interface Builder. This will create a new freeze dried object in the NIB file that will be instantiated when the NIB file gets loaded in.
- Select the object.
In the Inspector panel:
- Click on the Object Identity tab.
As we can see, the object is of class type NSObject. We need to change this to be MyWindowController to let this object be an instance of that type.
- In the class identity field, type in MyWindowController.
Our object is now of type MyWindowController. Tres bien!
Now that we’ve created a freeze dried MyWindowController object, it’s time to hook it up to our window instance.
Recall from the code listing on MyWindowController that we have defined an accessor — i.e. outlet — for our label, but have not yet set this property. In particular, we need to set the my_label property of our MyWindowController instance to point to the label of our window. In Cocoa jargon, we need to “plug our window’s label into our MyWindowController’s my_label outlet”.
In the MainMenu.xib window of Interface Builder:
- Hold down the right button (or hold ctrl+click) on the MyWindowController object and drag your mouse to the label in the designer.
- Release the right mouse button above the label. A popup menu as displayed below should now appear showing you the outlets to which you can hook the label up to.
- Click on my_label in the Outlet popup menu to hook the label in the window up to our MyWindowController’s my_label property.
Our MyWindowController now knows what object it should refer to when we use @my_label in MyWindowController. What remains is to set up the target-action for our button to our controller’s action handler method. We’re almost there indeed!
To set the target-action for our button:
- Hold down the right mouse button (or ctrl+click) on the button in our window and drag to our My Window Controller object in the MainMenu.xib window as illustrated below.
- Release the right mouse button. A popup menu should now appear displaying the controller’s action handler methods we can dispatch the button’s (click) action to.
- Click on unveil_full_message_clicked: to allow the button to dispatch the click action event to this method of our MyWindowController instance.
- Hit cmd+s to save our changes in Interface Builder.
- Return to Xcode. Make sure we’ve saved all the changes we’ve made to the files here too via cmd+s.
- Hit the Build & Run button in the toolbar as we’ve done before.
Congratulations, your very first MacRuby application should now be in working order! Go ahead, click on the button to unveil the full message of life!
Download code
BornToBeAlive MacRuby App Example
Epilogue
For the sake of brevity — something I know is hard to believe looking at the volume of this first tutorial — we’ve omitted a few steps you’d normally like to take into consideration.
For instance, we’ve not connected the window outlet of MyWindowController (an outlet it inherited from NSWindowController) to our window instance to allow it to manage the window. Connecting this outlet occurs in a similar fashion as was the case with connecting the label outlet by holding down the right mouse button on the MyWindowController instance and dragging it to the window instance.
Releasing the right mouse button will display a popup menu outlining the outlets we can connect the window to. Clearly we should select the window outlet here.
http://www.macruby.org/downloads.html
http://blog.phusion.nl/2010/03/12/creating-our-very-first-mac-application-with-ruby-how-exciting/
No comments:
Post a Comment