Tips & tricks for Obj-C
These are taken for an old blog of mine about Objective-C ...
Adding a trackingRect to a NSView object
This might be of interest (I spent a good hour trying to make it work); In the following code, I set a trackingRect to my entire NSView object, so I can track mouse entering and exiting my NSView :
- (id)initWithFrame:(NSRect)frameRect
{
self = [super initWithFrame:frameRect];
if (self != nil)
{
[self addTrackingRect:[self bounds] owner:self userData:nil assumeInside:NO];
}
return self;
}
Though perfectly correct, this code won't set the trackingRect as expected for the (not so) obvious reason that the NSView has not yet been moved to any window, and thus [self bounds] is just empty... The good thing to do, as I figured out, is to implement the setting up in the viewDidMoveToWindow method, as per the following :
-(void)viewDidMoveToWindow
{
[self removeTrackingRect:trackingRect];
NSPoint loc = [self convertPoint:[[self window] mouseLocationOutsideOfEventStream] fromView:nil];
BOOL inside = ([self hitTest:loc] == self);
trackingRect = [self addTrackingRect:[self bounds] owner:self userData:nil assumeInside:inside]
}
... which works fine ! A good idea is to clear the trackingRect when the view is released, that is, when moved to a nil NSWindow, with the following code :
- (void)viewWillMoveToWindow:(NSWindow *)window
{
if (!window && [self window]) [self removeTrackingRect:trackingRect];
[super viewWillMoveToWindow:window];
}
This will prevent the system from sending mouseEntered or mouseExited events to an already deallocated object... You're all set. Hope this helps!
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) {
// ...
}
}
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...
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 !
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...
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).