NSArrayControllers and binding – an example

NSArrayControllers can save you precious time (and lines) if your app basically deals with a NSMutableArray, in any form.

In this tutorial I’ll explain how to use NSArrayControllers to bind a data source and a view together and allow edition of the data.

The basics

For the set-up, you’ll first want to create a new Cocoa project, and create a custom class to hold your data. In this example, I will create a TaskList class, consisting of three instance variables : taskName, description and priority. As for now, I only need to write the getters and setters; my TaskList.h file will look like this :

#import <Cocoa/Cocoa.h>

@interface TaskList : NSObject  {
	NSString * taskName;
	int priority;
        NSString * description;
}

-(int)priority;
-(void)setPriority:(int)aValue;

-(NSString*)taskName;
-(void)setTaskName:(NSString*)aName;

-(NSString*)description;
-(void)setDescription:(NSString*)aDescription;

@end

And my TaskList.m file will look like this :

#import "TaskList.h"

@implementation TaskList 

-(id)init
{
     if ( self = [super init]) {
        [self setTaskName:@"New Task"];
        [self setPriority:1];
        [self setDescription:@"<To Do>"];
    }
    return self;
}

-(int)priority{ return priority;}
-(void)setPriority:(int)aValue { priority = aValue;}

-(NSString*)taskName{ return taskName;}
-(void)setTaskName:(NSString*)aName
{
    if (aName != taskName) {
        [taskName release];
        [aName retain];
        taskName = aName;
    }
}

-(NSString*)description{ return description; }
-(void)setDescription:(NSString*)aDescription
{
    if (aDescription != description) {
        [description release];
        [aDescription retain];
        description = aDescription;
    }
}
@end

Once this is done, we’ll set up the UI and add the NSArrayController in Interface Builder. The NSArrayController will basically take care of handling the data for us – but we will need to provide him with the datasource, and the bindings we want to use.

The UI

Double-click on MainMenu.nib to open it in Interface Builder. to the existing window, add a NSTableView with two column, an “Add” NSButton and a “Remove” NSButton. You can also add a NSTextField (that will hold the number of our objects) and another NSTextField that will hold the description for the task. An example is shown below :

Now add an NSArrayController (from the Cocoa-Controllers palette) to your Nib file. In the inspector palette for you NSArrayController, change the ‘Object Class Name’ to ‘TaskList’, and uncheck ‘Preserves selection‘. Add three keys below, taskName, priority and description. These will be the keys we’ll be using for the bindings.

The bindings

The first column of our table view should display, let’s say, our task name. Select the column, and in the bindings page of the inspector, bind value to the keyPath taskName of ‘arranged objects’ :

You can do the same for the other column, binding it to priority. Then, bind the description NSTextField value to the keyPath description of ‘arranged objects’, and the number NSTextField value to the keyPath @count of ‘arranged objects’.

Then, control-drag between the two buttons and the NSArrayController to make it the target, with the actions ‘insert’ and ‘remove’. You can also bind the remove button’s ‘enabled’ binding to the ‘canRemove’ attribute of the controller (this will prevent the user from clicking the button when the selection is null, for instance).

You’re all set ! Build and run, and your application is fully functional… (well, it doesn’t do much … but…):

If you want to add some saving and loading possibilities, you can now create a standard controller for your app, and make it conform to the NSCoding protocol …

An App without dock icon and menu bar

You might want to create an application that doesn’t appear in the dock or in the selector (Cmd-Tab), may it be because it’s a background application or a kind of widget you only want to show through a window for instance.

This is pretty simple: the only step is to add this key in the Info.plist file in your Cocoa Xcode project :

<key>LSUIElement</key>
<string>1</string>

An UIELement is an application that is background only but still has a UI (eg a floating utility window).

In the case of a ‘real’ background application, you might want to add this bit instead :

<key>LSBackgroundOnly</key>
<string>1</string>

As a matter of fact, the value set for LSBackgroundOnly overrides the value set for LSUIElement, thus setting both to 1 has the same effect than setting LSBackgroundOnly to 1…

Result : when running your application, no dock icon will show up. You won’t get a menu bar either (but you will get a window if you didn’t touch the nib file).

Dragging a file on your app icon in the dock

Last night I was wondering how to implement this feature – that is, to be able to drag a file on my application’s icon in the dock so I can basically get the filepath and process it in my app. The solution lies in a simple combo :

First of all, you need to implement a delegate method, application:openFile:, and process the filename. For instance :

- (BOOL) application:(NSApplication*)theApplication openFile:(NSString*)filename
{
    // for example, if fileToHandle is an instance variable ...
    fileToHandle = [filename retain];
    return YES;
}

The easiest way for this is to create a new Objective-C class implementing this method and set it as a delegate for your NSApplication (in InterfaceBuilder).

The second step is to enable some file types to be dragged on your application’s icon. To do this, you need to modify the Info.plist file in your project, adding the following lines :

<key>CFBundleDocumentTypes</key> 
   <array> 
     <dict> 
       <key>CFBundleTypeExtensions</key> 
       <array>  <string>rtf</string>  </array> 
     </dict> 
  </array> 

I put rtf but you can change that with any type you’d like to handle…

Getting the good info on mouseEvents …

When a user clicks somewhere on your NSView, you want to be able to determine several things :

1. First, where exactly did the user clicked? You can easily get that info from the method locationInWindow of the NSEvent class. You can also convert the point’s coordinates into another view’ coordinates system :

-(void)mouseDown:(NSEvent*)theEvent
{
	NSPoint mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil];
	NSLog(@" Mouse down at (%i,%i)", (int)mouseLoc.x, (int)mouseLoc.y);
}

You can also get the number of clicks with the following code :

     // ...
	int numberOfClicks = [theEvent clickCount];

2. Second : Did the mouse moved, and by how much ? This is directly inside the NSEvent argument that you have it :

-(void)mouseMoved:(NSEvent*)theEvent
{
	NSLog(@" Mouse has move by (%i,%i)", (int) [theEvent deltaX], (int) [theEvent deltaY]);
	
}

3. Now, suppose you want to kow if a mouse event occured (spatially) inside a predefined area, but don’t want to track the mouse entering or exiting this very area. Then you could define this area as a NSRect, and check if the mouse is inside the rectangle when clicked :

-(void)mouseDown:(NSEvent*)theEvent
{
	NSPoint mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil];

	// we suppose that myBounds is a non-nil NSRect 
        // defining your area of interest :
	if([self mouse:mouseLoc inRect:myBounds]) 
             // Do something
	else 
            // ...
}

(NB : for the mouseMoved methods to be called, the NSWindow in which the NSView is must accept mouseMoved events. You can make sure of that with following line (by default, NSWindows don’t accept mouseMoved events):

     // self represents you NSView object
      [[self window] setAcceptsMouseMovedEvents:YES];

Hope this helps !

Flipped composited images

I recently bumped in this ‘bug’ (?) while drawing an image and setting a trackingRect to catch users clicking on it. I’ve flipped the coordinates; in the drawRect method of my instance, I draw my image and set buttonBounds, an NSRect instance variable that holds the bounds of my button :

- (BOOL)isFlipped // allows us to work from top to bottom 
{
	return TRUE;
}

- (void)drawRect:(NSRect)aRect
{
   // ...

   [[NSImage imageNamed:@"myButton.png"] 
		compositeToPoint:NSMakePoint(100,100) 
		operation:NSCompositePlusLighter];
   // my image is a 16x16 PNG file
   myButtonBounds = NSMakeRect(100,100, 16,16);
	
}

and I then use my NSRect myButtonBounds to catch the click, and terminate the App :

-(void)mouseDown:(NSEvent*)theEvent
{
	NSPoint mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil];
	if([self mouse:mouseLoc inRect:myButtonBounds]) [NSApp terminate:self];
}

But this wouldn’t work : as the button is drawn from the left-bottom corner (to the right-top one), my NSRect is drawn .. from the left-top corner !! :

(the ‘close’ button is the image and I drew the NSRect with a grey border)

It seems to me like Cocoa is not taking the ‘flipped’ parameter into account when drawing images … or is it just a bug with compositeToPoint:operation: ? Or am I missing something ? … any clues ?

How to set up a basic Notification process

You’ve always wondered about NSNotifications … and the way to possibly use it to trigger actions. Notifications can be useful if you want to virtually ‘connect’ two or more objects that are totally separated in your code; thus, instead of establishing a hard-link through, let’s say, a controller or an instance variable, you can use notifications.

Notifications are messages that are processed via a central ‘center’ and dispatched upon observer objects. That is to say, an object A send a ‘doThat’ notification to the notification center, which then dispatches this notification to the object B that is known to be able to handle it (because you declared it). Let’s see how that works with NSNotifications :

First of all, you have to set up your object to be able to receive a notification. To do that, it must have a void method taking one single argument (a NSNotification pointer) to handle any notifications it may receive :

-(void)handleNotifications:(NSNotification*)aNotification
{
    NSLog(@" I've just received a %@ notification !", 
              [aNotification name]);
    // do something ...
}

Then, you have to declare your instance as an observer for the notification center, for a particular notification:

[[NSNotificationCenter defaultCenter] addObserver:self 
       selector:@selector(handleNotifications:)
       name:NSApplicationWillResignActiveNotification 
       object:nil];

That means that each time a NSApplicationWillResignActiveNotification is sent to the notification center, the method of your object will be called, allowing it to do something accordingly (for instance, hide the window).

Of course, you can declare your object as an observer for many different notifications, whether with the same selector (that tests the notification to know what to do) or with different ones (one for each different notification) :

// ...
[[NSNotificationCenter defaultCenter] addObserver:self 
       selector:@selector(handleBecomeActiveNotifications:)
       name:NSApplicationWillBecomeActiveNotification 
       object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self 
       selector:@selector(handleResignActiveNotifications:)
       name:NSApplicationDidResignActiveNotification 
       object:nil];

// ... and so forth

You can send (‘post’) notifications very simply with the following code :

[[NSNotificationCenter defaultCenter] postNotification:NSApplicationWillResignActiveNotification];

though in this case, sending a NSApplicationWillResignActiveNotification is not the best thing to do. But as you can create your own notifications, the possibilities are infinite…

Simple way to catch modifier keys

Here is a simple way to catch expected modifier keys for an event (in this example, mouseDown) :

-(void)mouseDown:(NSEvent*)theEvent
{
	if ([theEvent modifierFlags] & NSControlKeyMask) {
	    // Do something wise when the user has control-clicked ...
	}else if ([theEvent modifierFlags] & NSShiftKeyMask) {
	    // ...
        }
}

Talking about Cocoa

Here is a little blog dealing with Mac programming : snippets, technology, ...

Categories


Follow

Get every new post delivered to your Inbox.